Écrire des tests unitaires intégrés pour vos pipes Angular
Dans mon billet de blog précédent, nous avions appris qu’il existait deux types de tests unitaires pour les pipes Angular :
- les tests unitaires isolés (sans
TestBed
), - les tests unitaires intégrés (avec
TestBed
).
Nous avions mis l’accent sur des tests unitaires isolés. Dans cet article, nous allons mettre le focus sur la manière d’écrire des tests unitaires intégrés pour les pipes Angular.
On parle de tests unitaires intégrés lorsque nous testons le pipe dans les mêmes conditions que celles dans lesquelles il sera utilisé, c’est-à-dire dans le contexte d’un composant Angular.
Le pipe à tester
Au cas où vous n’auriez pas lu mon article précédent billet, voici le code du pipe que nous allons tester. Son but est de prendre en entrée un tableau d’entier et de retourner en sortie la moyenne des éléments qui le constituent.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'mean',})export class MeanPipe implements PipeTransform { transform(value: number[]): number { if (!Array.isArray(value)) { return value; } if (value.length === 0) { return undefined; } const sum = value.reduce((n: number, m: number) => n + m, 0); return sum / value.length; }}
On voit dans ce bout de code que:
- La classe
MeanPipe
implémente l’interfacePipeTransform
. - La méthode
transform
retourne la moyenne des éléments du tableauvalue
qui lui est passé en entrée.
Nous avons besoin d’un composant hôte
Vu que nous voulons écrire des tests intégrés pour notre pipe, nous avons besoin d’un composant hôte. Un composant hôte est comme tout autre composant Angular. Il n’a rien de spécial. C’est juste une façon d’utiliser le pipe dans un composant Angular.
Voici le code du composant hôte :
@Component({ template: '<div>{{ values | mean }}</div>',})export class MeanPipeHostComponent { values: number[];}
Ce composant hôte :
- déclare une propriété
values
qui va contenir les valeurs à transmettre à notre pipe, - affiche le résultat de la transformation de ces valeurs par le pipe
Le composant hôte n’existe que pour les besoins du test. Il ne faut donc pas l’importer dans nos modules applicatifs.
Mise en place des tests unitaires intégrés
La mise en place des tests unitaires intégrés est un peu complexe.
describe('MeanPipe inside a host component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [MeanPipe, MeanPipeHostComponent], }).compileComponents(); }));});
On commence avec un premier beforeEach
dans lequel on appelle la méthode TestBed.configureTestingModule()
.
Cette dernière nous permet de configurer un module à des fins de tests. Elle accepte à peu les mêmes paramètres que
le décorateur @NgModule
.
describe('MeanPipe inside a host component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [MeanPipe, MeanPipeHostComponent], }).compileComponents(); }));});
Le corps de notre beforeEach
est “wrappé” dans une zone Angular spéciale à travers l’appel à la fonction async
.
La fonction async
est l’une des nombreuses utilitaires de tests fournis par Angular. Nous en avons besoin, car la
méthode TestBet.compileComponents()
est asynchrone. Cela garantit que le template d’un composant défini via un
templateUrl
est compilé au préalable.
describe('MeanPipe inside a host component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [MeanPipe, MeanPipeHostComponent], }).compileComponents(); }));});
Dans notre exemple il n’était pas nécessaire d’utiliser TestBed.compileComponents()
car le template notre composant hôte est défini
inline.
Une fois la mise en place de notre module de test effectué, nous allons utiliser un autre bloc beforeEach
.
Nous commençons par utiliser la méthode TestBed.createComponent
à qui on passe en paramètre notre composant hôte
MeanPipeHostComponent
. Cela nous retourne un ComponentFixture
. Ce dernier est un “wrapper” autour de notre composant
hôte. Il nous permet entre autres de gérer son “change detection” et d’avoir accès à son injecteur.
describe('MeanPipe inside a host component', () => { let fixture: ComponentFixture<MeanPipeHostComponent>;
beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [MeanPipe, MeanPipeHostComponent], }).compileComponents(); }));
beforeEach(() => { fixture = TestBed.createComponent(MeanPipeHostComponent); });});
À partir de notre ComponentFixture
, nous pouvons obtenir un DebugElement
. Le DebugElement
est un “wrapper” autour
de l’élément HTML natif associé à notre composant. Il offre plus de fonctionnalités que l’élément HTMLElement
natif.
describe('MeanPipe inside a host component', () => { let fixture: ComponentFixture<MeanPipeHostComponent>; let debugElement: DebugElement;
beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [MeanPipe, MeanPipeHostComponent], }).compileComponents(); }));
beforeEach(() => { fixture = TestBed.createComponent(MeanPipeHostComponent); debugElement = fixture.debugElement; });});
Nous pouvons aussi obtenir une référence vers l’instance de notre composant hôte. Cela nous permettra de modifier des attributs du composant.
describe('MeanPipe inside a host component', () => { let fixture: ComponentFixture<MeanPipeHostComponent>; let debugElement: DebugElement; let component: MeanPipeHostComponent;
beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [MeanPipe, MeanPipeHostComponent], }).compileComponents(); }));
beforeEach(() => { fixture = TestBed.createComponent(MeanPipeHostComponent); debugElement = fixture.debugElement; component = fixture.componentInstance; });});
Les vrais tests
Maintenant, nous pouvons à écrire nos assertions.
it('should display 1', () => { component.values = [1, 1]; fixture.detectChanges();
const divDebugElement: DebugElement = debugElement.query(By.css('div')); const div: HTMLDivElement = divDebugElement.nativeElement;
expect(div.textContent.trim()).toEqual('1');});
Nous commençons par mettre à jour la propriété values
de notre composant.
it('should display 1', () => { component.values = [1, 1]; fixture.detectChanges();
const divDebugElement: DebugElement = debugElement.query(By.css('div')); const div: HTMLDivElement = divDebugElement.nativeElement;
expect(div.textContent.trim()).toEqual('1');});
Puis, nous appelons la méthode fixture.detectChanges()
qui va déclencher un “change detection”.
Cela aura pour effet la mise à jour du DOM pour refléter le changement opéré sur la propriété values
de notre composant hôte.
it('should display 1', () => { component.values = [1, 1]; fixture.detectChanges();
const divDebugElement: DebugElement = debugElement.query(By.css('div')); const div: HTMLDivElement = divDebugElement.nativeElement;
expect(div.textContent.trim()).toEqual('1');});
Ensuite nous utilisons la méthode query
de notre DebugElement
en lui passant en paramètre un prédicat By.css('div')
.
Cela nous permet de cibler l’élément HTML div
grâce à un sélecteur CSS.
it('should display 1', () => { component.values = [1, 1]; fixture.detectChanges();
const divDebugElement: DebugElement = debugElement.query(By.css('div')); const div: HTMLDivElement = divDebugElement.nativeElement;
expect(div.textContent.trim()).toEqual('1');});
Nous pouvons finir par une assertion vérifiant que la valeur qui est dans le DOM est bien celle qui est attendue.
it('should display 1', () => { component.values = [1, 1]; fixture.detectChanges();
const divDebugElement: DebugElement = debugElement.query(By.css('div')); const div: HTMLDivElement = divDebugElement.nativeElement;
expect(div.textContent.trim()).toEqual('1');});
Conclusion
Dans cet article, nous avons appris à écrire des tests unitaires intégrés pour nos pipes Angular. La mise en place de ce type de tests est plus compliquée que pour des tests unitaires isolés. Beaucoup de concepts sont en jeu notamment les utilitaires de tests Angular. Mais le jeu en vaut la chandelle, car les tests unitaires intégrés peuvent détecter des bogues que les tests unitaires isolés ne peuvent pas révéler.
Vous aimez ce blog ?
Suivez-moi sur Twitter pour plus de contenu !
Rejoignez la newsletter pour du contenu de grande qualité dans votre boite mail