J'essaie de comprendre comment se moquer d'une ElementRef
qui est injectée dans un composant. Mon composant est le suivant:
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);
}
}
et mes spécifications de test comme suit:
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();
}));
...
});
Lorsque les tests sont exécutés, la sortie du console.log
dans le constructeur de app.component.ts
est la suivante:
Comme vous pouvez le constater, cela consiste à injecter la MockAppService
mais pas la MockElementRef
(même s’ils se moquent tous les deux de la même manière).
Ce SO post suggère de le configurer comme vous le feriez avec d’autres simulacres, mais j’ai remarqué que c’était pour Angular 2 - alors je me demande si les choses ont changé dans Angular 4?
Vous pouvez trouver un Plunker avec le code ci-dessus et les tests Jasmine ici . Exécutez le Plunker puis cliquez sur le lien "Exécuter les tests unitaires" pour lancer les tests unitaires. La sortie de la console peut être observée dans les outils de développement/Firebug.
La réponse courte - c'est par conception :)
Examinons étape par étape la réponse plus longue et essayons de comprendre ce qui se passe sous le capot lorsque nous configurons un module de test via TestBed
.
Étape 1
Selon le code source de test_bed.ts :
configureTestingModule(moduleDef: TestModuleMetadata): void {
if (moduleDef.providers) {
this._providers.Push(...moduleDef.providers);
}
if (moduleDef.declarations) {
this._declarations.Push(...moduleDef.declarations);
}
// ...
}
Comme nous pouvons le constater - la méthode configureTestingModule
pousse simplement les instances fournies dans le tableau this._providers
. Et ensuite, nous pouvons dire: hé, TestBed
, donnez-moi ce fournisseur ElementRef
:
// ...
let elRef: ElementRef;
beforeEach(() => {
TestBed.configureTestingModule({
// ...
providers: [{provide: ElementRef, useValue: new MockElementRef()}]
});
// ...
elRef = TestBed.get(ElementRef);
});
it('test', () => {
console.log(elRef);
});
Dans la console, nous verrons:
La première console était consignée depuis le constructeur du composant et la seconde - depuis le test.Il semble donc que nous ayons affaire à deux instances différentes de ElementRef
. Allons-nous en.
Étape 2
Jetons un coup d'œil à un autre exemple et disons que nous avons un composant qui injecte ElementRef
et un autre service personnalisé AppService
que nous avons créé auparavant:
export class HelloComponent {
constructor(private _elementRef: ElementRef, private _appService: AppService) {
console.log(this._elementRef);
console.log(this._appService);
}
}
Lorsque nous testons ce composant - nous devons fournir AppService
(le service lui-même ou sa maquette), MAIS, si nous ne fournissons pas ElementRef
à TestBed
- le test ne s'en plaindra jamais: NullInjectorError: No provider for ElementRef!
.
Nous pouvons donc proposer que ElementRef
ne ressemble pas à une dépendance et soit toujours lié au composant lui-même. Nous nous rapprochons de la réponse. :)
Étape 3
Examinons de plus près comment TestBed
crée le composant: TestBed.createComponent(AppComponent)
. Ceci est une version très simplifiée du code source:
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);
// ...
}
Donc, nous devons aller de l'avant et vérifier l'implémentation de la classe ComponentFixture
dans le code source :
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;
// ...
Nous pouvons voir que elementRef
est une propriété de la classe ComponentFixture
qui initialise le constructeur.
Et enfin, résumant ce qui précède - nous avons la réponse: ElementRef
qui est injecté au composant dans le constructeur est en réalité un wrapper autour de l’élément DOM. L'instance injectée de ElementRef
est une référence à l'élément Host du composant actuel. Suivez ce message StackOverflow _ pour obtenir plus d'informations à ce sujet.
C'est pourquoi, dans le constructeur de composant console.log, nous voyons l'instance de ElementRef
et non l'instance de MockElementRef
. Ainsi, ce que nous avons réellement fourni dans le tableau de fournisseurs TestBed - n’est qu’une autre instance de ElementRef
basée sur MockElementRef
.