I'm trying to figure out how to mock an ElementRef
that is injected into a component. My component is as follows:
app.component.ts:
import { Component, ElementRef } from '@angular/core'; import { AppService } from './app.service'; @Component({ selector: 'app-root', templateUrl: './app/app.component.html', styleUrls: ['./app/app.component.css'] }) export class AppComponent { title = 'app works!'; constructor(private _elementRef: ElementRef, private _appService: AppService) { console.log(this._elementRef); console.log(this._appService); } }
and my test spec as follows:
app.component.spec.ts:
import { TestBed, async } from '@angular/core/testing'; import { ElementRef, Injectable } from '@angular/core'; import { AppComponent } from './app.component'; import { AppService } from './app.service'; @Injectable() export class MockElementRef { nativeElement: {} } @Injectable() export class MockAppService { } describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], providers: [ {provide: ElementRef, useClass: MockElementRef}, {provide: AppService, useClass: MockAppService} ] }).compileComponents(); })); ... });
When the tests are ran, the output from the console.log
's in constructor of app.component.ts
is:
As you can see, it's injecting the MockAppService
but not the MockElementRef
(even though they are both mocked in the same way).
This SO post suggests to set it up as you would any other mock, however I've noticed that this is for Angular 2 - so am wondering if things have changed in Angular 4?
A Plunker with the above code and Jasmine tests can be found here. Run the Plunker then click the "Run Unit Tests" link in order to start the unit tests. The console output can be observed in developer tools/Firebug.
The short answer - it's by design :)
Let's dive into the longer answer step by step and try to figure out - what's going on under the hood when we configure a Testing Module via TestBed
.
Step 1
According to the source code of test_bed.ts:
configureTestingModule(moduleDef: TestModuleMetadata): void { if (moduleDef.providers) { this._providers.push(...moduleDef.providers); } if (moduleDef.declarations) { this._declarations.push(...moduleDef.declarations); } // ... }
As we can see - configureTestingModule
method simply pushes the provided instances into the this._providers
array. And then we can say: hey, TestBed
, give me this provider ElementRef
:
// ... let elRef: ElementRef; beforeEach(() => { TestBed.configureTestingModule({ // ... providers: [{provide: ElementRef, useValue: new MockElementRef()}] }); // ... elRef = TestBed.get(ElementRef); }); it('test', () => { console.log(elRef); });
In the console we'll see:
First console was logged from component constructor and the second one - from the test. So, it seems like we're dealing with two different instances of ElementRef
. Let's move on.
Step 2
Let's take a look at another example and say we have a component which injects ElementRef
and some other custom service AppService
which we created before:
export class HelloComponent { constructor(private _elementRef: ElementRef, private _appService: AppService) { console.log(this._elementRef); console.log(this._appService); } }
When we test this component - we must provide AppService
(the service itself or its mock), BUT, if we don't provide ElementRef
to TestBed
- the test will never complain about this: NullInjectorError: No provider for ElementRef!
.
So, we can propose, that ElementRef
doesn't look like a dependency and always linked to the component itself. We're getting closer to the answer. :)
Step 3
Let's take a closer look on how TestBed
creates component: TestBed.createComponent(AppComponent)
. This is a very simplified version from the source code:
createComponent<T>(component: Type<T>): ComponentFixture<T> { this._initIfNeeded(); const componentFactory = this._compiler.getComponentFactory(component); // ... const componentRef = componentFactory.create(Injector.NULL, [], `#${rootElId}`, this._moduleRef); return new ComponentFixture<T>(componentRef, ngZone, autoDetect); // ... }
So, we have to go forward and check the implementation of ComponentFixture
class in the source code:
export class ComponentFixture<T> { // The DebugElement associated with the root element of this component. debugElement: DebugElement; // The instance of the root component class. componentInstance: T; // The native element at the root of the component. nativeElement: any; // The ElementRef for the element at the root of the component. elementRef: ElementRef; // ... constructor( public componentRef: ComponentRef<T>, public ngZone: NgZone|null, private _autoDetect: boolean) { this.changeDetectorRef = componentRef.changeDetectorRef; this.elementRef = componentRef.location; // ...
We can see, that elementRef
is a property of ComponentFixture
class which is initialized the constructor.
And finally, summarizing the above - we've got the answer: ElementRef
which is injected to the component in the constructor is actually a wrapper around the DOM element. The injected instance of ElementRef
is a reference to the host element of the current component. Follow this StackOverflow post to get more information about it.
This is why in the component constructor console.log we see the instance of ElementRef
and not the instance of MockElementRef
. So, what we actually provided in the TestBed providers array - is just another instance of ElementRef
based on MockElementRef
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With