Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 4: Mock ElementRef

Tags:

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:

console output

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.

like image 557
Ian A Avatar asked Dec 18 '17 16:12

Ian A


1 Answers

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:

enter image description here

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.

like image 150
shohrukh Avatar answered Oct 16 '22 17:10

shohrukh