En situation rĂ©elle, vous serez amenĂ©s Ă tester des composants plus complexes que ce qu’on a vu dans la premiĂšre partie. Par exemple, supposons que l’on souhaite tester une barre latĂ©rale contenant un menu. On aimerait pouvoir la tester sans se soucier du dit menu. Dans de telles situations, on pourra utiliser ce que l’on appelle des « tests superficiels », ou comme disent les anglophones, des « shallow tests ». Ces tests superficiels permettent de tester les composants sur un seul niveau de profondeur, en ignorant tout Ă©lĂ©ment enfant que l’Ă©lĂ©ment pourrait contenir. On dit qu’on teste le composant en « isolation ».
Plan global du cours
- â Introduction aux tests Angular
- ⥠Premier test unitaire avec Jasmine
- âą Tester des composants Angular (1/2)
- ⣠Tester des composants Angular (2/2)
- †Tester des directives Angular (1/2)
- â„ Tester des directives Angular (2/2)
- ⊠Tester des pipes Angular
- ⧠Tester des services Angular
Markup
Retrouvez le code de cet article sur ce dépÎt stackblitz :
Ou suivez pas Ă pas les instructions đ !
Un composant un peu plus complexe
Faisons évoluer notre composant précédent (ContactFormComponent
) en lui greffant un service qui fera une requĂȘte vers un serveur qui se chargera de dĂ©livrer le message. Quel serveur, comme il va procĂ©der, tout ceci nous en avons cure pour nos tests, nous allons simplement simuler les requĂȘtes sortantes et leurs rĂ©ponses correspondantes.
ApiService
Créons donc notre service ApiService
(pour faire simple đ) en prenant comme modĂšle le code ci-aprĂšs :
import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
@Injectable({
providedIn: 'root'
})
export class ApiService {
apiPath = 'http://apiserver.lol';
constructor(private http: HttpClient) {
}
sendEmail(payload: {recipient: string, message: string}): Observable<any> {
return this.http.post<any>(`${this.apiPath}/send-email`, payload);
}
}
Snippet 4.1 â Service ApiService.
Ensuite on va pouvoir injecter notre service flambant neuf directement dans le constructeur du composant, merci l’injection de dĂ©pendance :
constructor(private apiService: ApiService) {
this.message = new FormControl('', Validators.required);
this.contactForm = new FormGroup<any>({
message: this.message
});
}
à partir de là , si vous avez la curiosité de lancer la suite de tests, vous devriez obtenir une magnifique erreur, semblable à celle de la figure 4.1.
Avec ce cas d’utilisation typique, on va bien s’amuser car nous injectons dans notre composant ContactFormComponent
un service (ApiService
) qui lui mĂȘme possĂšde une dĂ©pendance (HttpClient
). On a donc deux niveaux de dĂ©pendances (figure 4.2), configuration idĂ©ale pour commencer Ă faire n’importe quoi đ. Heureusement, on va analyser tout ça dans le dĂ©tail pour aborder les problĂšmes potentiels du bon cĂŽtĂ©.
Les outils de tests d’Angular
Parce qu’on s’apprĂȘte Ă tester un composant un peu plus complexe, je me dois d’abord de faire le tour des principaux outils qu’Angular met Ă notre disposition pour nous aider Ă Ă©crire des tests tout mignon. Ces outils sont des dĂ©pendances provenant du framework Angular ou fournies par ce dernier, il faudra donc les importer en dĂ©but de test.
Le « mal-aimé »
Commençons donc par le « mal-aimĂ© » (non ce n’est pas Louis XV) :
import { DebugElement } from '@angular/core';
On utilisera DebugElement
pour inspecter un Ă©lĂ©ment du DOM pendant le test. On peut le considĂ©rer comme le « HTMLElement » natif avec des mĂ©thodes et des propriĂ©tĂ©s supplĂ©mentaires qui peuvent ĂȘtre utiles pour le dĂ©boggage des Ă©lĂ©ments. Si vous avez lu le chapitre prĂ©cĂ©dent, vous savez dĂ©jĂ qu’on ne l’utilisera pas souvent. đ
Les outils du quotidien
Ensuite les outils du quotidien :
import { ComponentFixture â , fakeAsync âĄ, TestBed âą, tick ⣠} from '@angular/core/testing';
Bien sĂ»r ne mettez pas les numĂ©ros, ils sont lĂ pour l’explication suivante :
- â â une « fixture » de composant est un objet qui permet d’interagir avec une instance du dit composant, dans l’environnement de test. Il fournit un moyen de simuler le comportement du composant et de tester ses fonctionnalitĂ©s, sans avoir besoin d’un navigateur ou d’un serveur.
- ⥠â l’utilisation de
fakeAsync
garantit que toutes les tĂąches asynchrones sont terminĂ©es avant d’exĂ©cuter les assertions. Ne pas utiliserfakeAsync
en prĂ©sence d’un code asynchrone peut entraĂźner l’Ă©chec du test car les assertions peuvent ĂȘtre exĂ©cutĂ©es avant que toutes les tĂąches asynchrones ne soient terminĂ©es.fakeAsync
s’utilise de concert avectick
pour simuler le passage du temps. Il accepte un paramĂštre, qui est le nombre de millisecondes pour avancer le temps. On y reviendra plus tard. - âą â on utilisera cette classe pour installer et configurer les tests. Ătant donnĂ© qu’on voudra utiliser
TestBed
pour chaque nouveau test unitaire d’un composant, d’une directive ou mĂȘme d’un service, c’est l’un des utilitaires les plus importants fournis par Angular. A travers ce document, nous analyserons les mĂ©thodesconfigureTestingModule
,overrideModule
etcreateComponent
, que nous utiliserons ensuite. Ayez en tĂȘte que l’API deTestBed
est trĂšs Ă©tendue, nous ne ferons qu’effleurer sa surface de l’API dans ce document. N’hĂ©sitez pas Ă consulter la documentation officielle. - ⣠â
tick
, voir point âĄ
SĂ©lecteurs css
Ensuite, et c’est Ă contre coeur que je le fais, voyons la star des sĂ©lecteurs css :
import { By } from '@angular/platform-browser';
By
est une classe incluse dans le module @angular/platform-browser
qui sert à sélectionner des éléments du DOM. Pour sélectionner un élément avec un sélecteur css, on procÚde de la façon suivante :
By.css('.my-class')
La classe offre en tout 3 méthodes, regroupés dans la liste suivante :
MĂ©thode | Description | ParamĂštre |
---|---|---|
all | Retourne tous les éléments | Aucun |
css | Retourne que les éléments ciblés | Attribut css |
directive | Retourne les éléments qui possÚde la directive | Nom de la directive |
LĂ encore on s’en servira avec parcimonie, uniquement avec un pistolet pointĂ© sur la tempe. đ«
Animations
C’est connu, dans les tests on se fiche des animations et les dĂ©veloppeurs Angular l’ont anticipĂ© :
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
On utilisera donc la classe NoopAnimationsModule
pour simuler des animations, ce qui permet aux tests de s’exĂ©cuter rapidement sans attendre la fin des animations.
Les routes
Dans les tests, il est fortement dĂ©conseillĂ© d’utiliser les routes de l’application. Le module RouterTestingModule
remplacera au pied levé le module RouterModule
:
import { RouterTestingModule } from '@angular/router/testing';
GrĂące Ă ce module, nous dĂ©finirons nos propres routes pour les tests, meilleur moyen de ne pas tout casser si le nom d’une route venait Ă changer⊠đ€.
Petit récapitulatif
Pour ceux qui lisent en diagonale, voici la liste récapitulative des outils abordés dans ce paragraphe :
import { DebugElement } from '@angular/core'; â
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; âĄ
import { By } from '@angular/platform-browser'; âą
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; âŁ
import { RouterTestingModule } from '@angular/router/testing'; â€
- â â utilisĂ© pour le dĂ©boggage des Ă©lĂ©ments du composant.
- ⥠â les outils du quotidien fourni par la team Angular.
- âą â sĂ©lection des Ă©lĂ©ments du DOM avec un sĂ©lecteur css.
- ⣠â supprimer les animations inutiles lors des tests.
- †â utilisĂ© pour tester la configuration du router.
Tout ceci est encore un peu flou, mais Ă ce stade c’est tout Ă fait normal.
Peut ĂȘtre l’avez vous remarquĂ©, mais il n’existe pas de module FormsTestingModule
, qui remplacerait le module FormsModule utilisĂ© dans l’application. Mais en y rĂ©flĂ©chissant un peu, Ă quoi bon ? đ
Construction du test pas Ă pas
Structure générale du test
Nous allons procĂ©der comme lors de l’article prĂ©cĂ©dent, c’est Ă dire qu’on va utiliser un « squelette » et l’alimenter petit Ă petit :
import { ComponentFixture, TestBed } from '@angular/core/testing'; â
import { ContactFormComponent } from './contact-form.component';
import { FormGroup } from "@angular/forms";
describe('ContactFormComponent', () => { âĄ
let component: ContactFormComponent; âą
let contactForm: FormGroup;
let fixture: ComponentFixture<ContactFormComponent>;
beforeEach(async () => { âŁ
// todo
});
// -- tests -- //
it('', () => {}); â€
it('', () => {});
afterEach(async () => { â„
// todo
});
});
Snippet 4.2 â contact-form.component.spec.ts, le squelette des tests.
- â â import des dĂ©pendances, du framework et celles du composant Ă tester.
- ⥠â dĂ©claration de la suite de tests du composant Ă tester.
- âą â dĂ©claration des variables dont la portĂ©e sera limitĂ©e au bloc
describe
de premier niveau (celui juste avant) :
âcomponent
: l’instance du composant.
âcontactForm
: l’instance du formulaire.
âfixture
: on peut voir cet objet comme une « enveloppe » autour de l’instance du composant, qui permet d’accĂ©der Ă ses mĂ©thodes, propriĂ©tĂ©s etc. - ⣠â fonction callback appelĂ©e avant chaque test unitaire.
- †â les tests unitaires eux mĂȘmes.
- â„ â fonction callback appelĂ©e aprĂšs chaque test unitaire.
Construire l’environnement de test
Contrairement Ă l’article prĂ©cĂ©dent, on ne va pas pouvoir faire l’Ă©conomie du module de test fourni par Angular, car cette fois-ci, nous avons des dĂ©pendances injectĂ©es dans notre composant. On ne va pas non plus initialiser une application Angular complĂšte, ça serait beaucoup trop lourd. On va donc s’appuyer sur un module de test qui va se charger de faire la « glue » entre tous les composants et services donc nous avons besoin. Ce module est la classe TestBed
.
On peut voir la classe TestBed
comme un support pour nos tests. Au lieu d’instancier le framework, on va utiliser une instance de la classe TestBed
qui va se charger de coordonner toutes les dépendances du composant à tester : services, directives, pipes etc. Bien sûr il y aura quelques manipulations à effectuer manuellement (par exemple TestBed
n’exĂ©cute pas la mĂ©thode « ngOnInit » tout seul, il faut le faire manuellement), mais nous reverrons ça plus tard.
TestBed
va donc crĂ©er un module de test, et comme tout module Angular il faut le configurer. Bonne nouvelle pour nous, la configuration de ce module reprend la mĂȘme logique que les modules « classiques » d’Angular, telle qu’on peut la voir dans le fichier app.module.ts
(snippet 4.3).
@NgModule({
declarations: [AppComponent],
imports: [
AppRoutingModule,
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Snippet 4.3 â Configuration d’un module sous Angular.
Ainsi il faudra aussi s’assurer qu’Ă chaque test une nouvelle instance de la classe TestBed
(le module de test) soit disponible, la fonction beforeEach
est donc l’endroit idoine pour arriver Ă nos fins đ. CrĂ©ons donc notre premier callbak beforeEach
:
beforeEach(async () => {
await TestBed.configureTestingModule({ â
imports: [], âĄ
declarations: [ContactFormComponent], âą
providers: [], âŁ
schemas: [] â€
}).compileComponents();
fixture = TestBed.createComponent(ContactFormComponent); â„
component = fixture.componentInstance; âŠ
contactForm = component.contactForm; â§
fixture.detectChanges(); âš
});
- â â configure et crĂ©e le module de test qui va nous permettre de tester le composant. L’objet passĂ© en paramĂštre est de type
TestModuleMetadata
. - ⥠â la propriĂ©tĂ© « imports » est utilisĂ©e pour importer les modules requis par le composant, mais ici nous n’en avons pas besoin pour le moment.
- âą â la propriĂ©tĂ© « declarations » est utilisĂ©e pour dĂ©clarer les composants, les directives et les pipes requis par le composant testĂ©. Pour le moment on ne dĂ©clare que le minimum, Ă savoir le composant Ă tester lui mĂȘme.
- ⣠â la propriĂ©tĂ© « providers » permet de configurer l’injection de dĂ©pendance, de la mĂȘme maniĂšre que tout module Angular. Plus tard c’est ici qu’on dĂ©clarera des « fakes services » ou « mocks services ».
- †â la propriĂ©tĂ© « schemas » (facultative) permet de configurer le schĂ©ma utilisĂ© pour compiler et valider le modĂš le du composant lors des tests. Les plus courants sont
CUSTOM_ELEMENTS_SCHEMA
etNO_ERRORS_SCHEMA
pour autoriser certaines propriétés seulement. Par exemple, le schémaNO_ERRORS_SCHEMA
permettra Ă tout Ă©lĂ©ment qui va ĂȘtre testĂ© d’avoir n’importe quelle propriĂ©tĂ© (Ă Ă©viter).
Mais le but de ce document n’Ă©tant pas d’expliciter cette notion, on n’ira pas plus loin. - â„ â crĂ©ation de la « fixture » du composant (voir plus haut la dĂ©finition).
- ⊠â crĂ©ation d’une rĂ©fĂ©rence Ă l’instance du composant avec une variable qui a une portĂ©e Ă©tendue Ă toute la suite de tests.
- ⧠â mĂȘme chose pour le formulaire du composant, ça Ă©vitera les noms Ă rallonge dans les tests.
- âš â la mĂ©thode
detectChanges
dĂ©clenche un cycle de dĂ©tection des modifications pour le composant; il est nĂ©cessaire de l’appeler aprĂšs avoir initialisĂ© un composant ou modifiĂ© une valeur de propriĂ©tĂ©. AprĂšs avoir appelĂ©detectChanges
, les mises Ă jour du composant seront rendues dans le DOM. En production, Angular possĂšde un mĂ©canisme qui dĂ©termine quand exĂ©cuter la dĂ©tection des modifications, mais il est absent des tests unitaires. C’est pourquoi nous devons le faire frĂ©quemment entre chaque test unitaire.
Tout ceci est bien gentil mais nous n’avons toujours pas rĂ©solu notre problĂšme initial, Ă savoir la double dĂ©pendance du composant Ă tester (voir figure 4.2). Pour ceux qui ont suivi, le point ⣠nous apprends que la propriĂ©tĂ© « imports » configure l’injection de dĂ©pendances. Parfait, c’est ce dont nous avons besoin. Injectons donc manuellement notre service ApiService
et sa dépendance HttpClientTestingModule
:
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule], â
declarations: [ContactFormComponent],
providers: [ApiService], âĄ
schemas: []
}).compileComponents();
// ...
});
Snippet 4.4 â Configuration de l’injection de dĂ©pendance.
- â â import du module
HttpClientTestingModule
, spĂ©cialement taillĂ© pour les tests. Il fait la mĂȘme chose que son cousinHttpClientModule
, mais en faisant des requĂȘtes fictives (pour les tests c’est mieux đ€). - ⥠â on « informe » l’injection de dĂ©pendance d’utiliser l’implĂ©mentation rĂ©elle du service
ApiService
.
On relance les tests, et la magie opĂšre enfin :
Peut ĂȘtre l’avez-vous remarquĂ© au point â , mais en plus du module HttpClientTestingModule
, on injecte aussi FormsModule
et ReactiveFormsModule
. Pourquoi ça ? Rappelez-vous : le composant qu’on teste embarque un formulaire rĂ©actif, qui dĂ©pend de ces 2 modules. Nous devons donc les importer aussi ici.
Bien, l’environnement de test est prĂȘt đŸ. Maintenant il faut se poser la question cruciale : que voulons-nous tester ? Pour le moment rien car le service ApiService
ne fait rien du tout dans notre composant đ. RemĂ©dions Ă cette situation intenable : modifions la mĂ©thode submitForm
qui va se servir du service ApiService
pour envoyer le message (histoire qu’on n’ai pas fait tout ça pour rien đ) :
submitForm() {
if (this.contactForm.valid) {
this.success = true;
this.error = false;
} else {
this.success = false;
this.error = true;
}
}
submitForm() {
if (this.contactForm.valid) { â
this.apiService.sendEmail({ âĄ
recipient: 'toto@toto.com',
message: this.message.value,
}).subscribe({
next: result => { âą
this.success = true;
this.error = false;
},
error: e => { âŁ
this.success = false;
this.error = true;
}
}
)
}
}
- â â on vĂ©rifie que le formulaire est valide, sinon on ne fait rien.
- ⥠â on fait appel au service
ApiService
pour envoyer le message, avec les bons arguments passés à la méthodesendEmail
. - âą â dĂ©finition du callback Ă exĂ©cuter si la requĂȘte aboutit : affiche du message de confirmation.
- ⣠â dĂ©finition du callback Ă exĂ©cuter si la requĂȘte renvoie une erreur : affiche du message d’erreur.
Ă gauche la mĂ©thode telle qu’elle Ă©tait lors du prĂ©cĂ©dent article, Ă droite les modifications avec le service ApiService
. La différence est majeure :
- Ă gauche c’est la validitĂ© du formulaire qui dĂ©termine ce qu’on affiche : si le formulaire est valide, on affiche le message de confirmation. Sinon le message d’erreur.
- Ă droite, c’est le type de la rĂ©ponse qui dĂ©termine le message Ă afficher : si on reçoit un « Observable » en erreur, on affiche le message d’erreur. Sinon ce sera le message de confirmation.
Donc Ă partir de maintenant, quand on clique sur « Envoyer », Angular va tenter de faire une requĂȘte vers http://apiserver.lol, qui va bien sĂ»r Ă©chouer, mais lĂ n’est pas notre soucis. Nous devons rĂ©parer le fichier de tests, qui ne se sent pas trĂšs bien depuis qu’on a fait le mĂ©nage dans la mĂ©thode submitForm
:
Si on en croit Jasmine, les messages de confirmation n’apparaissent plus. Rien d’Ă©tonnant Ă cela, grĂące Ă l’utilisation du module HttpClientTestingModule
, toute requĂȘte Http est interceptĂ©e sans lever la moindre erreur. Du coup le point âą et ⣠ne sera jamais exĂ©cutĂ©, quel que soit l’issu de la requĂȘte Http. Comment s’en sortir ? C’est simple, comme toujours se poser les bonnes questions, Ă savoir qu’est ce que je teste ?
- je veux m’assurer que mon composant affiche un message de confirmation si le message est envoyĂ©.
- je veux aussi m’assurer que mon composant affiche un message d’erreur si le message n’est pas envoyĂ©.
Si on analyse de prĂȘt ces 2 phrases, on s’aperçoit qu’on ne parle :
- ni de requĂȘte Http
- ni de service
ApiService
Eh oui, on ne test pas (encore) ApiService
et encore moins les requĂȘtes http d’Angular. Alors que faire ? L’idĂ©e ici est de dĂ©coupler le composant de ses dĂ©pendances embarrassantes, de cette façon elles ne provoqueront plus d’erreurs. On dit qu’on va faire un test en isolation. Mais le composant a quand mĂȘme besoin de ces dĂ©pendances, nous allons les remplacer par des « dĂ©pendances factices »
DĂ©pendance factice
Une « dĂ©pendance factice » est une technique de test qui permet de crĂ©er une fausse implĂ©mentation d’un service dont dĂ©pend le composant Ă tester (ou d’autres services), plutĂŽt que d’utiliser l’implĂ©mentation rĂ©elle du service.
La simulation d’un service est utile car elle permet d’isoler le comportement du composant testĂ© du comportement des dĂ©pendances sur lesquelles il repose. Cela facilite le test du composant (il est isolĂ©), sans se soucier du comportement des services dont il dĂ©pend. De plus, « mocker » un service permet de contrĂŽler le comportement du service dans le test, ce qui peut ĂȘtre utile pour tester diffĂ©rents scĂ©narios ou conditions d’erreur.
Alors la question que tout le monde se pose : comment fait-on ? Rappelez-vous du point ⥠du snippet 4.3 : Angular nous donne la possibilitĂ© de configurer l’injection de dĂ©pendance đ. A partir de lĂ , et faites moi confiance sur ce point, on a deux solutions :
- on ré-écrit un object
ApiService
complet. - on utilise une fonctionnalité offerte par Jasmine : les espions.
Les deux sont possibles, mais nous ne verrons que la seconde solution qui consistera Ă :
- crĂ©er et placer un espion sur l’implĂ©mentation rĂ©elle de la dĂ©pendance.
- informer l’espion de quelle mĂ©thode il doit espionner.
- et, c’est lĂ le plus intĂ©ressant, lui dire que faire si la mĂ©thode est appelĂ©e lors du test.
- injecter l’espion Ă la place du service rĂ©el.
Evidemment, comme toujours, on va créer un espion par test, nous serons donc avisés de placer sa définition dans un callback beforeEach
. Allons y :
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { throwError } from "rxjs"; â
import { FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { ContactFormComponent } from './contact-form.component';
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { ApiService } from "../../services/api.service";
describe('ContactFormComponent', () => {
let component: ContactFormComponent;
let contactForm: FormGroup;
let fixture: ComponentFixture<ContactFormComponent>;
let apiSpy: any; âĄ
beforeEach(async () => {
apiSpy = jasmine.createSpyObj<ApiService>('ApiService', ['sendEmail']); âą
apiSpy.sendEmail.and.returnValue(of(null).pipe(delay(1000))); âŁ
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
declarations: [ContactFormComponent],
providers: [
{provide: ApiService, useValue: apiSpy} â€
],
schemas: []
}).compileComponents();
fixture = TestBed.createComponent(ContactFormComponent);
fixture.detectChanges();
component = fixture.componentInstance;
contactForm = component.contactForm;
});
// --
});
Snippet 4.5 â CrĂ©ation d’un espion.
Je l’avoue le callback beforeEach
commence à prendre du poids, promis on remédiera à tout ça par la suite. En attendant, quelques éclairages :
- â â import de l’opĂ©rateur throwError qui va simuler une erreur http.
- ⥠â dĂ©claration de l’espion
apiSpy
qu’on « type » en any pour Ă©viter les ennuis. - âą â dĂ©finition de l’espion avec la mĂ©thode
jasmine.createSpyObj
du framework Jasmine : on espionnera la mĂ©thode « sendEmail » - ⣠â dĂ©finition du comportement de l’espion quand la mĂ©thode « sendEmail » sera appelĂ©e : il va renvoyer un Observable aprĂšs une seconde de dĂ©lai.
- †â on informe le module de test d’utiliser notre espion plutĂŽt que le « vrai » service
ApiService
en paramĂ©trant l’injection de dĂ©pendances.
On enregistre le tout et on se rue sur les tests : pas de chance ça ne fonctionne toujours pas đ±.
Ce comportement n’est pas Ă©tonnant : la mĂ©thode submitForm
est dĂ©sormais asynchrone puisqu’elle fait appelle au service ApiService
qui doit faire requĂȘte Http. Et comme toutes les requĂȘtes Http, on s’attend Ă recevoir soit un Observable soit une Promise, les deux Ă©tants asynchrones. Et pour se rapprocher encore un peu plus de la rĂ©alitĂ© et comprendre ce que l’on fait, on a insĂ©rĂ© un dĂ©lai d’une seconde avant l’Ă©mission de la rĂ©ponse. C’est pour cette raison que le test n’aboutit pas : le code « n’a pas le temps d’aboutir ».
RemĂ©dions Ă cet Ă©pineux problĂšme qui en a fait suer plus d’un đ. Nous allons en quelques sortes forcer le temps Ă s’Ă©couler plus vite, grĂące Ă la fonction fakeAsync
(fournie par Angular) qui permet de prendre le contrÎle du flux temporel. Rien que ça me direz-vous !
Il suffit de rĂ©-Ă©crire le test et d’injecter directement le callback dans l’unique argument de fakeAsync
:
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; â
// --
describe('ContactFormComponent', () => {
let component: ContactFormComponent;
let contactForm: FormGroup;
let fixture: ComponentFixture<ContactFormComponent>;
let apiSpy: any;
beforeEach(async () => {
// --
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
describe('Form', () => {
// --
});
describe('Component behavior', () => {
it('shoud display an error message if the email is not sent', fakeAsync(() => {
contactForm.setValue({message: ''});
component.submitForm();
tick(1000);
expect(component.error).toBeTrue();
expect(component.success).toBeFalse();
}));
fit('shoud display a success message if the email is sent', fakeAsync(() => { âĄ
contactForm.setValue({message: 'Hi there!'});
component.submitForm();
tick(1000); âą
expect(component.error).toBeFalse();
expect(component.success).toBeTrue();
}));
// --
});
});
Analysons un petit peu ce nouveau charabia :
- â â importation de 2 nouvelles dĂ©pendances :
fakeAsync
ettick
. - ⥠â injection du callback du test directement dans
fakeAsync
. On substitueit
Ăfit
pour se focaliser uniquement sur ce test pour le moment. - âą â on force le passage d’une seconde.
Bingo ! Le test passe à présent :
La fonction tick
est une mĂ©thode fournie par le framework de test d’Angular qui permet de simuler le passage du temps.
Pour tester un composant avec du code asynchrone tel que des promesses ou des observables, la fonction tick
peut ĂȘtre utilisĂ©e pour faire avancer l’horloge virtuelle de l’environnement de test afin de simuler le passage du temps jusqu’Ă la fin de l’opĂ©ration asynchrone.
Quid des autres tests ? Malheureusement on a toujours un petit récalcitrant :
Mais pourquoi le message d’erreur ne s’affiche t’il pas ? On a pourtant bien utilisĂ© la fonction fakeAsync
en conjonction avec tick
, mais rien ne se passe comme prévu ?
C’est bien heureux je dois dire, car rappelez-vous le point ⥠du snippet 4.5. On a paramĂ©trĂ© la mĂ©thode « sendEmail » du service ApiService
pour qu’elle renvoie un Observable aprĂšs un dĂ©lai d’une seconde. En d’autres termes, la requĂȘte aboutit dans tous les cas. Jamais d’erreur. C’est fĂącheux car pour notre test, nous avons justement besoin d’Ă©mettre une erreur !
Bien entendu, nous avions tout prévu en amont et grùce à Jasmine on va pouvoir illustrer la phrase suivante, écrite un peu plus haut :
De plus, « mocker » un service permet de contrÎler le comportement du service dans le test.
Donc pour ce test, uniquement celui-ci, on va changer la valeur de retour de « sendEmail » et lui de renvoyer un « Observable » en erreur :
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { delay, of, throwError } from "rxjs"; â
describe('ContactFormComponent', () => {
let component: ContactFormComponent;
let contactForm: FormGroup;
let fixture: ComponentFixture<ContactFormComponent>;
let apiSpy: any;
beforeEach(async () => {
// --
});
// --
describe('Form', () => {
// --
});
describe('Component behavior', () => {
fit('shoud display an error message if the email is not sent', fakeAsync(() => {
apiSpy.sendEmail.and.returnValue(throwError(() => new Error('TEST')).pipe(delay(1000))); âĄ
contactForm.setValue({message: 'Hi there!'});
component.submitForm();
tick(1000);
expect(component.error).toBeTrue();
expect(component.success).toBeFalse();
}));
});
});
- â â importation de l’opĂ©rateur
throwError
qui sera utilisĂ© pour gĂ©nĂ©rer un « Observable » en erreur. - ⥠â on redĂ©fini la valeur de retour de l’espion, ici un « Observable » en erreur.
DĂ©finir la valeur de retour de l’espion directement dans le test est une bonne pratique. Elle permet au lecteur de tout de suite connaĂźtre l’origine de l’erreur.
AprĂšs un tel traitement, le test devrait de nouveau fonctionner :
Et voilĂ ! Que de chemin parcouru depuis le premier article de ce document. Je dois dire que je me perds parfois dans des digressions mais ce n’est jamais pour rien. Pour conclure, voici le code complet du test :
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { delay, of, throwError } from "rxjs";
import { ContactFormComponent } from './contact-form.component';
import { ApiService } from "../../services/api.service";
describe('ContactFormComponent', () => {
let component: ContactFormComponent;
let contactForm: FormGroup;
let fixture: ComponentFixture<ContactFormComponent>;
let apiSpy: any;
beforeEach(async () => {
apiSpy = jasmine.createSpyObj<ApiService>('ApiService', ['sendEmail']);
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
declarations: [ContactFormComponent],
providers: [
{provide: ApiService, useValue: apiSpy}
],
schemas: []
}).compileComponents();
fixture = TestBed.createComponent(ContactFormComponent);
fixture.detectChanges();
component = fixture.componentInstance;
contactForm = component.contactForm;
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
describe('Form', () => {
it('should validate the form if « message » field is not empty', () => {
contactForm.setValue({
message: 'hello'
});
expect(contactForm.valid).toBeTruthy();
});
it('should not validate the form if « message » field is empty', () => {
contactForm.setValue({
message: ''
});
expect(contactForm.valid).toBeFalsy();
});
});
describe('Component behavior', () => {
beforeEach(() => {
apiSpy.sendEmail.and.returnValue(of(null).pipe(delay(1000)));
});
it('shoud display an error message if the email is not sent', fakeAsync(() => {
apiSpy.sendEmail.and.returnValue(throwError(() => new Error('TEST')).pipe(delay(1000)));
contactForm.setValue({message: 'Hi there!'});
component.submitForm();
tick(1000);
expect(component.error).toBeTrue();
expect(component.success).toBeFalse();
}));
it('shoud display a success message if the email is sent', fakeAsync(() => {
contactForm.setValue({message: 'Hi there!'});
component.submitForm();
tick(1000);
expect(component.error).toBeFalse();
expect(component.success).toBeTrue();
}));
it('shoud not display any message after init', () => {
expect(component.error).toBeFalse();
expect(component.success).toBeFalse();
});
});
});
Snippet 4.6 â Le test complet du composant ContactForm.
Ce que nous avons appris
- tester un composant Angular revient souvent à tester ses méthodes, en laissant de cÎté autant que possible la partie « template ».
- en utilisant la fonction
fakeAsync
, il est possible de s’assurer que tous les appels asynchrones sont terminĂ©s avant que les assertions ne soient exĂ©cutĂ©es. Cela empĂȘche le test d’Ă©chouer de maniĂšre inattendue. TestBed
est la classe principale du framework de test d’Angular. Elle fournie un environnement de test qui permet de manipuler toutes les autres classes d’Angular, donc les composants bien sĂ»r.