Le test unitaire avec Angular4
Le test unitaire avec Angular4
- ne nous le cachons pas
- c'est un peu douloureux. Lorsque l'on connaît les performances et les capacités de Karma
- on ne peut que constater que le côté 'unitaire' des tests est un peu mis de côté au profit de gigantesques mocks.
Étant donné qu'il n'est pas possible d'instancier unitairement un composant
- nous nous retrouvons dans l'obligation de mocker un contexte angular complet (On se croirait un peu dans du test de conf Spring sur les bords).
Mais le test
- c'est important
- et puisqu'une solution existe alors restons sur le principe que c'est mieux que rien ! C'est parti !
Le concept du test de composant
il faut bien comprendre le concept derrière les tests unitaires proposés par le framework Angular4. Tout d'abord
- la pierre angulaire de votre application
- pour rappel
- c'est votre fichier app.module.ts.
Ce fichier
- c'est un peu la bibliothèque centrale de votre application. Vous y déclarez vos composants et l'endroit ou ils se trouvent
- les librairies externes dont vous aurez besoin
- les URLS internes à votre application... Bref
- c'est le coeur de votre application. Votre composant n'est pas déclaré au sein de votre NgModule ? Alors il n'existe pas. Vous avez la dépendance de votre librairie externe mais n'avez pas ajouté sa déclaration dans le NgModule ? Cette librairie externe n'est pas dans votre projet.
Et pour les tests ?
Et bien
- pour les tests... On en a aussi besoin
- de ce noyau central ! Et là... C'est un peu là ou le bas blesse
- parce que vous allez devoir utiliser le mock fourni par Angular et déclarer les éléments dont votre composant a besoin pour fonctionner.
Exemple ! Prenons un composant simple :
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-component'
-
templateUrl: './app.component.html'
-
styleUrls: ['./app.component.css']
})
export class AppComponent {
title: string = 'toto';
}
app.component.html
<h1>{{ title }}</h1>
Pour tester le remplacement du titre dans une balise de type h1
- je vais devoir rédiger le test suivant (explications plus bas) :
app.component.spec.ts
import { ComponentFixture
- TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { AppComponent } from '../../../app/app.component';
describe('Test Component :'
- () => {
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent ] // declare the test component
});
fixture = TestBed.createComponent(AppComponent); // AppComponent View test instance
comp = fixture.componentInstance; // AppComponent Model test instance
});
it('h1 tag should have been filled with component title variable'
- () => {
fixture.detectChanges();
const el = fixture.debugElement.query(By.css('h1')).nativeElement;
expect(el.textContent).toContain(comp.title);
expect(el.textContent).toEqual('toto');
});
});
Si la syntaxe des tests vous perturbe
- je vous invite à lire l'article sur les tests avec Jasmine.
Voici le minimum pour tester le composant plus haut. Et ce composant ne contenait rien de particulier. Juste un titre.
Plus de détails !
Le TestBed
- c'est le mock du core Angular. Et le mock de votre noyau app.module.ts
- c'est le bout de code suivant :
TestBed.configureTestingModule({
declarations: [ AppComponent ]
});
Dans notre cas
- il est petit
- mais sa taille peut varier si vous ne faites pas assez attention ! En passant
- je vous conseille le lire l'article sur le mock de composants enfant dans les tests.
Il fonctionne comme votre @NgModule dans votre fichier app.module.ts. Vous disposez des champs :
TestBed.configureTestingModule({
imports: [ HttpModule ]
-
declarations: [ AppComponent ]
-
providers: [ AppService ]
});
Si vous devez tester un formulaire qui lance une requête de création à un de vos services
- alors votre fichier de test ressemblera à ceci :
app.formComponent.spec.ts
import { ComponentFixture
- TestBed } from '@angular/core/testing';
import { FormBuilder
- FormsModule
- ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ApplicationService } from '../../../app/services/applicationService';
import { FormComponent } from '../../../app/components/app.formComponent';
describe('Creation Form Component Test :'
- () => {
let comp: FormComponent;
let fixture: ComponentFixture<ApplicationForm>;
let applicationService: ApplicationService;
let spy: jasmine.Spy;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ FormsModule
- ReactiveFormsModule
- HttpModule ]
-
declarations: [ FormComponent ]
-
providers: [ ApplicationService
- FormBuilder ]
});
fixture = TestBed.createComponent(FormComponent);
comp = fixture.componentInstance;
// Get the applicationService actually injected into the component
applicationService = fixture.debugElement.injector.get(ApplicationService);
});
});
Ici
- on constate que l'on a du indiquer à notre mock de NgModule
- les modules dont on aurait besoin pour nos tests unitaires :
TestBed.configureTestingModule({
imports: [ FormsModule
- ReactiveFormsModule
- HttpModule ]
-
declarations: [ FormComponent ]
-
providers: [ ApplicationService
- FormBuilder ]
});
Nous devons déclarer les modules de formulaire dynamique et les modules HTTP et gérer les classes injectées (providers) à passer à notre composant de test.
Le test du modèle
Notre classe de test a instancié deux variables : comp
- qui représente le modèle
- et fixture
- qui représente la vue.
app.component.spec.ts
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent ] // declare the test component
});
fixture = TestBed.createComponent(AppComponent); // AppComponent View test instance
comp = fixture.componentInstance; // AppComponent Model test instance
});
Pour tester les méthodes de votre modèle
- le componentInstance sera votre meilleur allié. C'est même assez facile avec Jasmine. Rajoutons une méthode à notre composant !
app.component.ts
import { Component
- Output
- EventEmitter } from '@angular/core';
@Component({
selector: 'app-component'
-
templateUrl: './app.component.html'
-
styleUrls: ['./app.component.css']
})
export class AppComponent {
title: string = 'toto';
@Output() onUpdate: EventEmitter<string> = new EventEmitter<string>();
updateView() {
this.title = 'tata';
this.onUpdate.emit(this.title);
}
}
Pour explication
- sur un appel à la méthode de mise à jour
- le titre va changer et un événement de mise à jour sera émis. Si un autre composant est abonné à cet événement
- il lancera une méthode qu'il a défini.
Testons le changement de titre et l'émission de l'événement :
app.component.spec.ts
import { ComponentFixture
- TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { AppComponent } from '../../../app/app.component';
describe('Test Component :'
- () => {
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent ] // declare the test component
});
fixture = TestBed.createComponent(AppComponent); // AppComponent View test instance
comp = fixture.componentInstance; // AppComponent Model test instance
});
it('h1 tag should have change when update method is called'
- () => {
comp.updateView();
fixture.detectChanges();
const el = fixture.debugElement.query(By.css('h1')).nativeElement;
expect(el.textContent).toContain(comp.title); // Check that the content of the view is still the same as the model.
expect(el.textContent).toEqual('tata'); // Check that the content of the view was updated.
});
it('an update event should have been emitted when update method is called'
- () => {
spyOn(comp.onUpdate
- 'emit'); // Spy on the emitter 'emit' method.
comp.updateView();
expect(comp.onUpdate.emit).toHaveBeenCalledWith('tata'); // Check that the emit method was called with the right value given.
});
});
J'ai préféré faire deux méthodes pour expliciter le sens de ce qui est testé. A vous de voir la finesse de vos tests unitaires !
Le test de la vue
Tester la vue
- c'est un peu plus laborieux
- je trouve. Surtout lorsque l'on cherche à tester certains champs de formulaires. Prenons un exemple simple
- nous ferons des articles spécifiques pour ces champs.
Allez
- on garde le composant de la partie précédente
- et on rajoute un bouton qui appelle la méthode :
app.component.ts
import { Component
- Output
- EventEmitter } from '@angular/core';
@Component({
selector: 'app-component'
-
templateUrl: './app.component.html'
-
styleUrls: ['./app.component.css']
})
export class AppComponent {
title: string = 'toto';
@Output() onUpdate: EventEmitter<string> = new EventEmitter<string>();
updateView() {
this.title = 'tata';
this.onUpdate.emit(this.title);
}
}
app.component.html
<h1>{{ title }}</h1>
<button (click)="updateView();">Validate</button>
Et voici le test :
app.component.spec.ts
import { ComponentFixture
- TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { AppComponent } from '../../../app/app.component';
describe('Test Component :'
- () => {
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent ] // declare the test component
});
fixture = TestBed.createComponent(AppComponent); // AppComponent View test instance
comp = fixture.componentInstance; // AppComponent Model test instance
});
it('click on validate button should trigger the update method'
- () => {
spyOn(comp
- 'updateView'); // Spy on the emitter 'emit' method.
// trigger the click
const button = fixture.debugElement.query(By.css('button')).nativeElement;
button.dispatchEvent(new Event('click'));
expect(comp.updateView).toHaveBeenCalled();
});
});
Nous venons de tester que la vue déclenche bien l'appel vers la méthode lors du clic !
Bref
- vous comprenez maintenant la mécanique derrière le test unitaire avec Angular4 !