Tester unitairement un service Angular

Tester unitairement un service Angular

Les tests unitaires représentent une part importante du développement.

Si vous êtes adeptes du TDD (Test Driven Development), je ne vous apprendrai pas leur utilité.

Dans cet article, je vais vous présenter comment tester un service Angular.

En particulier, si votre service nécessite l’injection d’un autre service, vous verrez comment réaliser ce qu’on appelle un mock.

Prérequis

Si vous souhaitez réaliser les exercices au fur et à mesure de la lecture de cet article, vous devez disposer des outils suivants installés sur votre poste :

OutilVersion
NodeJS10.19.0
NPM6.14.3
Angular9.1.0

Si ce n’est pas le cas, vous pouvez vous référer à la documentation officielle de chacun.

Le code source complet des exemples ci-dessous est disponible sur GitHub : https://github.com/benjaminprevot/2020-04-03-tester-unitairement-un-service-angular

Rappels

Test unitaire

Pour simplifier, les tests unitaires permettent de valider une partie précise de votre programme.

En particulier, ils évitent les régressions lors du cycle de développement.

Intégrés dans une chaîne d’intégration continue, ils représentent une étape importante de la validation du code source.

Mock

Les mocks sont des objets dits “simulés”.

Ils permettent de gérer le comportement d’une instance en forçant sont comportement.

Lors de la mise en place de tests unitaires, ils sont très utiles pour définir un comportement immuable dans le temps d’objets qui sont utilisés au sein de votre code.

Cela évite des comportements imprévisibles qui fausseraient les résultats des tests.

Création de l’application

Passons maintenant à la pratique.

Tout d’abord, nous allons créer une nouvelle application Angular pour vous guider dans la mise en place des tests unitaires.

Pour cela, vous devez ouvrir un terminal de commande et vous positionnez dans le répertoire de travail.

Dans mon cas, je me place dans le répetoire workspace de mon répertoire personnel, sous Linux.

cd ~/workspace

Ensuite, la création de l’application mon-application se fait via la commande ci-dessous :

ng new mon-application

Quelques questions vous serons posées, leurs réponses n’ont pas d’importance ici.

Lorsque l’exécution est terminée, il faut maintenant se placer dans la répertoire de l’application (c’est dans ce répertoire que les commandes suivantes seront lancées).

cd mon-application

Notre appliation est maintenant créée et prête à être testée.

Création du service

Nous allons maintenant créer le service et le test unitaire correspondant.

Pour cela, Angular met à disposition une commande permettant de créer le fichier contenant la définition du service et le fichier pour le test unitaire correspondant.

ng generate service MonService

Je ne rentrerai pas dans le détail de cette commande, vous pouvez vous référer à la documentation officielle pour plus de détails : https://angular.io/cli/generate#service

Dans le répertoire src/app, nous disposons de 2 fichiers supplémentaires :

  • mon-service.ts : définition de notre service
  • mon-service.spec.tx : test unitaire de notre service

Par défaut, le test unitaire contient le code pour vérifier que le service est correctement créé.

Exécution des tests unitaires

Pour lancer les tests unitaires, il faut utiliser la commande suivante :

ng test

Vous verrez alors les différentes étapes de compilation et l’exécution des tests.

Une fenêtre de votre navigateur s’ouvre avec un rapport d’exécution.

En particulier, la liste des tests unitaires et leur état est disponible.

Rapport des tests unitaires initial
Rapport des tests unitaires initial

Les autres informations de ce rapport ne nous intéressent pas ici.

Comme vous avez pu le remarquer, la console est “bloquée”.

En effet, la commande précédente lance un processus permettant de rafraichir le rapport au fur et à mesure que le code est modifié.

Nous pouvons maintenant faire évoluer le service et compléter les tests.

Ajout d’un nouveau test unitaire

Afin respecter le principe du TDD, nous allons ajouter un test vérifier le résultat de notre service.

Pour cela, nous allons vérifier que l’appel à la fonction get de notre service retourne la valeur Hello World!.

Il faut alors éditer le fichier src/app/mon-service.spec.tx.

Nous rajoutons le bloc suivant :

it('should return a promise of "Hello World!" when get is called', (done) => {
  service.get().then(result => {
    expect(result).toEqual('Hello World!');
    done();
  });
});

Je ne rentrai pas dans le détail de la syntaxe.

Petites précisions sur ce test :

  • Le résultat de la fonction get est en fait de type Promise<string>. C’est pourquoi, il faut utiliser then() afin de s’abonner au résultat.
  • L’exécution est asynchrone, il faut mettre en place le paramètre done et notifier de la fin de l’exécution du test via done().

Si vous enregistrer cette modification, le rapport des tests n’est pas mis à jour puisqu’il y a une erreur dans la console.

error TS2339: Property 'get' does not exist on type 'MonServiceService'.

En effet, nous appelons la fonction get() dans le test unitaire alors qu’elle n’est pas définie dans le service.

Nous allons maintenant la mettre en place pour pouvoir faire fonctionner le test.

Création de la fonction get()

Ouvrez maintenant le fichier src/app/mon-service.ts.

Pour définir la fonction get(), il suffit de rajouter le code ci-dessous :

get(): Promise<string> {
  return null;
}

Cette fois, notre code compile et le rapport des tests est mis à jour.

Rapport des tests unitaires suite à la création de get()
Rapport des tests unitaires suite à la création de get()

Nous obtenons une erreur indiquant que notre résultat est null.

C’est bien ce que nous avons écrit.

Implémentation basique

Afin de faire fonctionner notre service, nous allons remplacer la déclaration de la fonction.

Pour l’instant, nous allons utiliser retourner Hello World! à chaque appel de la fonction.

get(): Promise<string> {
  return Promise.resolve('Hello World!');
}

Lorsqu’on enregistre les modifications, il n’y a plus d’erreur dans la console et le rapport est à jour indiquant que tous les tests ont réussi.

Rapport des tests unitaires suite à l'implémentation basique de get()
Rapport des tests unitaires suite à l'implémentation basique de get()

Nous allons améliorer un peu notre service afin qu’il appelle une API et retourne le résultat.

Mock d’un appel d’API

Comme précédemment, nous allons d’abort améliorer notre test unitaire.

Notre service va appeler une API, mais nous n’allons pas réellement réaliser l’appel HTTP.

En effet, il faudrait mettre un place un serveur web dédié au test et il s’agirait plus d’un test d’intégration que d’un test unitaire.

Notre but ici est de vérifier que le résultat de l’API est bien retourné par notre service.

Nous allons donc mettre un place un mock de l’API afin de simuler son comportement.

L’appel de l’API sera fait via HttpClient.

Déclaration du mock

Tout d’abord, nous allons déclarer le mock au début du bloc describe.

let httpClientMock: jasmine.SpyObj<HttpClient>;

N’oubliez pas d’ajouter l’import de HttpClient.

import { HttpClient } from '@angular/common/http';

Instanciation du mock

Nous ajoutons maintenant un block beforeEach pour instancier le mock.

beforeEach(() => {
  httpClientMock = jasmine.createSpyObj('HttpClient', [ 'get' ]);
});

On indique ici que nous allons réaliser un mock de type HttpClient pour la fonction get.

Il est possible d’instancier le mock directement au moment de la déclaration.

J’ai une préférence pour le faire dans un bloc beforeEach afin de réinitialiser le mock à chaque test.

Ajout du provider

Il faut maintenant indiquer que le mock doit être utilisé lors de l’injection.

Pour cela, il faut modifier l’appel à la méthode configureTestingModule.

TestBed.configureTestingModule({
  providers: [
    { provide: HttpClient, useValue: httpClientMock }
  ]
});

Ainsi, à chaque injection de HttpClient, c’est notre mock qui sera pris en compte.

Amélioration du test unitaire

Nous modifions maintenant notre test afin de :

  • Simuler le comportement de l’appel à l’API
  • Vérifier que le résultat de cet appel est retourné par notre service

On remplace le test précédent par ce code :

it('should return a promise of "Résultat API" when get is called', (done) => {
  httpClientMock.get.withArgs('/api').and.returnValue(of('Résultat API'));

  service.get().then(result => {
    expect(result).toEqual('Résultat API');
    done();
  });
});

Il faut également ajouter l’import

import { of } from 'rxjs';

La ligne concernant le mock simule l’appel à la fonction get lorsqu’elle est appelée avec le paramètre /api pour qu’elle retourne Résultat API.

En sauvegardant, on obtient une erreur indiquant que le résultat n’est pas celui attendu.

Rapport des tests unitaires suite à l'amélioration de get()
Rapport des tests unitaires suite à l'amélioration de get()

En effet, nous n’avons pas changé l’implémentation de notre service.

Il retourne toujours Hello World! alors que nous attendons Résultat API.

Correction de l’implémentation

Nous allons maintenant utiliser HttpClient dans notre service.

Dans notre service, nous ajoutons l’injection de HttpClient en modifiant le constructeur :

constructor(private httpClient : HttpClient) { }

N’oubliez par l’import

import { HttpClient } from '@angular/common/http';

Enfin, il faut remplacer la fonction get() comme ci-dessous :

get(): Promise<string> {
  return this.httpClient.get<string>('/api').toPromise();
}

On précise ici que nous appelons l’API /api et que nous retournons son résultat sous forme de Promise.

En sauvegardant, on obtient le rapport suivant :

Rapport final des tests unitaires pour get()
Rapport final des tests unitaires pour get()

Conclusion

Nous avons vu ici comment tester un service Angular et réaliser un mock.

Pour HttpClient, Angular met à disposition un module de testing intégré. Le but ici est de donner une façon de faire qui peut être appliquée quelque soit le type souhaité.

Je vous mets à disposition les sources complètes sur GitHub : https://github.com/benjaminprevot/2020-04-03-tester-unitairement-un-service-angular