É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:

  1. La classe MeanPipe implémente l’interface PipeTransform.
  2. La méthode transform retourne la moyenne des éléments du tableau value 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

Pas de spam. Que du contenu de qualité.