Testez la récupération

Dans le chapitre précédent, vous avez écrit vos premiers tests avec Jasmine mais sans les outils Angular. Pour un service sans dépendance, ça peut fonctionner, mais pour tous les autres éléments d'une application Angular (services avec dépendances, components, directives…), vous aurez besoin du TestBed.

Avant de regarder comment utiliser le TestBed, parlons un peu de notre stratégie globale de tests.

Découvrez la stratégie de tests

Regardons cet écran :

Un écran de l'application où l'utilisateur voit le statut de toutes les tables du restaurant
L'écran de toutes les tables

Ici, on a un comportement à tester : "est-ce que, pour une réponse serveur donnée, l'application affiche les tables correctement ?"

Quelles briques de l'application jouent un rôle dans ce comportement ?

  • ApiTableService  contient la requête HTTP

  • ApiTableFacade  expose les use cases de l'application : elle permet aux components d'accéder aux données et d'effectuer des actions, sans avoir à appeler directement les services

  • TableStatusList  est un smart component qui appelle la façade pour récupérer les données et les dispatch vers les components enfants (dumb components)

Techniquement, il serait tout à fait envisageable de placer le test sur  TableStatusList  et de tester toute la chaîne, simplement en utilisant une fausse requête HTTP (que vous découvrirez bientôt !). À une époque, c'était même ce que je recommandais, mais voici pourquoi j'ai changé d'avis :

  • un seul test a trop de raisons d'échouer — il y a tellement d'éléments différents dans la chaîne, et comprendre pourquoi le test échoue devient plus compliqué

  • la suite de tests devient lourde à configurer — il faut mettre en place toutes les dépendances de tous les éléments, et le TestBed devient immense : ça ralentit les tests, et ça rend les fichiers de tests beaucoup moins faciles à lire et à comprendre

Du coup, voici la stratégie de tests que je vous propose :

  • une suite de tests côté components qui utilise une fausse implémentation de la façade : ces tests vérifient que les components affichent correctement les différents jeux de données de test

  • une suite de tests côté façade qui utilise de fausses requêtes HTTP : ces tests vérifient que votre vraie façade interagit correctement avec le serveur

La façade devient ce que j'appelle le pivot point.

Dans ce chapitre, vous allez implémenter le côté façade, et dans le chapitre suivant le côté component !

Testez la façade

Pour l'instant, on va continuer avec la structure où on place les fichiers de tests à côté des fichiers que l'on teste. Dans le dosser  facades  , créez le fichier  api-table.facade.spec.ts  :

describe('ApiTableFacade', () => {
  
});

Si on regarde la façade elle-même, on voit tout de suite la ligne qui nous empêche de l'instancier simplement :

private apiService = inject(TableService);

Voilà pourquoi on aura besoin du TestBed. La structure de base ressemble à ceci :

import { ApiTableFacade } from './api-table.facade';
import { TestBed } from '@angular/core/testing';

describe('ApiTableFacade', () => {
  let facade: ApiTableFacade;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [ApiTableFacade]
    });

    facade = TestBed.inject(ApiTableFacade);
  });
  
});

Regardons ce qui se passe dans le  beforeEach  :

  • on crée un testing module avec le TestBed, déclarant notre façade comme provider

  • on utilise  inject  pour associer l'instance du testing module à notre variable  facade

Commençons par un smoke test — un test qui vérifie que la façade s'instancie correctement :

import { ApiTableFacade } from './api-table.facade';
import { TestBed } from '@angular/core/testing';

describe('ApiTableFacade', () => {
  let facade: ApiTableFacade;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [ApiTableFacade]
    });

    facade = TestBed.inject(ApiTableFacade);
  });

  it('should be created', () => {
    expect(facade).toBeTruthy();
  });

});

J'utilise très souvent ce genre de test pour mettre en place une suite sur du code déjà existant, car les différentes erreurs que Jasmine remonte permet d'identifier les dépendances et potentiellement de détecter des red flags.

Exécutez vos tests et vous aurez l'erreur suivante :

Error: NG0908: In this configuration Angular requires Zone.js
error properties: Object({ code: 908 })

Cette erreur vient du fait que l'application TableMaster est Zoneless, c'est-à-dire qu'elle n'utilise pas Zone.js. Cette approche est devenue stable avec Angular v20 et modifie légèrement l'approche, entre autres, des tests unitaires.

Pour résoudre cette erreur, ajoutez le provider suivant :

TestBed.configureTestingModule({
    providers: [
        ApiTableFacade,
        provideZonelessChangeDetection()
    ]
});

Ce qui vous donne… une nouvelle erreur !

ɵNotFound: NG0201: No provider found for `TableService`. Source: DynamicTestModule. Path: ApiTableFacade2 -> TableService.

On voit cette erreur parce que ApiTableFacade dépend de TableService. Si on regarde dans  app.config.ts  , on voit comment l'application fournit TableService :

{ provide: TableService, useClass: ApiTableService },

Du coup, ajoutez ce même provider à votre testing module :

TestBed.configureTestingModule({
    providers: [
        ApiTableFacade,
        provideZonelessChangeDetection(),
        { provide: TableService, useClass: ApiTableService }
    ]
});

Et on arrive à notre dernière erreur !

ɵNotFound: NG0201: No provider found for `_HttpClient`. Source: DynamicTestModule. Path: ApiTableFacade2 -> TableService -> _HttpClient.

ApiTableService a une dépendance sur HttpClient — ce qui est parfaitement logique ! — donc fournissons-le :

TestBed.configureTestingModule({
    providers: [
        ApiTableFacade,
        provideZonelessChangeDetection(),
        { provide: TableService, useClass: ApiTableService },
        provideHttpClient()
    ]
});

Enfin, notre smoke test passe !

Bon, du coup, quel comportement on va tester ? Et surtout, comment on fait ?!

Testez un premier comportement HTTP

Le premier comportement que je vous propose de tester vient de la méthode  allTables  de la façade : si les tables n'ont pas encore été chargées, elles doivent être chargées depuis le serveur.

Seul hic : on a dit qu'il fallait que les tests soient rapides et isolés. Ça sous-entend qu'on ne va pas appeler un vrai serveur. On va donc utiliser un outil Angular pour intercepter les requêtes HTTP et les contrôler nous-mêmes.

HttpClientTesting

Ajoutez le provider suivant à votre testing module :

TestBed.configureTestingModule({
    providers: [
        ApiTableFacade,
        provideZonelessChangeDetection(),
        { provide: TableService, useClass: ApiTableService },
        provideHttpClient(),
        provideHttpClientTesting()
    ]
});

Ce provider nous donne accès, entre autres, au HttpTestingController. Vous découvrirez ci-dessous comment l'utiliser, mais commencez déjà par y accéder :

describe('ApiTableFacade', () => {
  let facade: ApiTableFacade;
  let httpCtrl: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ApiTableFacade,
        provideZonelessChangeDetection(),
        { provide: TableService, useClass: ApiTableService },
        provideHttpClient(),
        provideHttpClientTesting()
      ]
    });

    facade = TestBed.inject(ApiTableFacade);
    httpCtrl = TestBed.inject(HttpTestingController);
  });

  it('should be created', () => {
    expect(facade).toBeTruthy();
  });

});

Dans une suite de tests qui gère des requêtes HTTP, il est très utile, après chaque test, de confirmer qu'il ne reste pas de requêtes ouvertes non traitées : cela permet d'identifier des requêtes inattendues ! Ajoutez donc un  afterEach  et appelez la méthode  verify()  du HttpTestingController :

describe('ApiTableFacade', () => {
  let facade: ApiTableFacade;
  let httpCtrl: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ApiTableFacade,
        provideZonelessChangeDetection(),
        { provide: TableService, useClass: ApiTableService },
        provideHttpClient(),
        provideHttpClientTesting()
      ]
    });

    facade = TestBed.inject(ApiTableFacade);
    httpCtrl = TestBed.inject(HttpTestingController);
  });

  it('should be created', () => {
    expect(facade).toBeTruthy();
  });

  afterEach(() => {
    httpCtrl.verify();
  });

})

Maintenant on peut enfin écrire un test !

Testez une requête

Commençons par décrire notre comportement :

it('should load all tables if they are not already loaded', () => {
    
});

Pensons à nos trois étapes : Arrange, Act, Assert.

La première étape est déjà implémentée grâce à notre TestBed et beforeEach, donc passons à Act. Quelle action doit-on déclencher pour notre test ? 

it('should load all tables if they are not already loaded', () => {
    const tables = facade.allTables();
});

Si vous exécutez les tests dès maintenant, vous aurez une erreur :

Error: Expected no open requests, found 1: GET /api/tables

C'est notre fameux  afterEach  ! On voit bien que la requête attendue est bien envoyée, donc on peut utiliser HttpTestingController pour y répondre :

it('should load all tables if they are not already loaded', () => {
    const tables = facade.allTables();
    const req = httpCtrl.expectOne('/api/tables');
});

Comme son nom l'indique,  expectOne  explique au HttpTestingController qu'on attend une requête à l'URL passé en argument — d'ailleurs, vous remarquerez que l'erreur précédente a bien disparu. expectOne  nous donne aussi accès à cette requête, et on va s'en servir pour y répondre avec sa méthode  flush, en y passant les données factices  TEST_TABLES  (du dossier  test-data  ) :

it('should load all tables if they are not already loaded', () => {
    const tables = facade.allTables();
    const req = httpCtrl.expectOne('/api/tables');
    req.flush(TEST_TABLES);
});

L'étape Act est terminée : que nous faut-il pour l'étape Assert ? Dans ce cas précis, aucune transformation n'est appliquée sur la réponse du serveur, donc on veut vérifier que le Signal  tables  contienne le même tableau de  Table  :

it('should load all tables if they are not already loaded', () => {
    const tables = facade.allTables();
    const req = httpCtrl.expectOne('/api/tables');
    req.flush(TEST_TABLES);
    
    expect(tables()).toEqual(TEST_TABLES);
});

Et là le test passe !

toBe or… toEqual ?

Dans ce test, on a utilisé la méthode  toEqual  fournie par Jasmine. Il existe également une méthode  toBe  . Il est essentiel de comprendre la différence entre les deux :

  • toBe  est une comparaison d'égalité stricte (comme  ===  ), et donc ne peut être utilisée pour comparer deux objets ou deux tableaux

  • toEqual  est une comparaison d'égalité profonde, c'est-à-dire que deux objets dont toutes les paires clefs-valeurs sont identiques (ou deux tableaux ayant exactement les mêmes éléments dans le même ordre) seront considérés comme égaux

Prenons du recul

Pensons un instant au chemin de code du dernier test :

  • on appelle la méthode publique  allTables

  • allTables  appelle la méthode privée  loadTables

  • loadTables  appelle la méthode  getTables  du service et, une fois la réponse reçue, met à jour le Signal  tableState

  • allTables  retourne  tableState  comme Signal readonly

Ce chemin implique deux choses importantes :

  • on peut modifier n'importe quel élément sur ce chemin tant que le résultat ne change pas, et le test continuera de passer — je pense notamment à la possibilité d'implémenter une solution de Store, par exemple — car le test n'est pas couplé à l'implémentation

  • on ne ressent pas le besoin de tester une méthode privée directement, car elle est de toute manière vérifiée par un test

J'espère que vous commencez à voir la puissance des tests !

À vous de jouer

Contexte

Pour cette fin de chapitre, plutôt que d'écrire un test pour une requête HTTP, je vous propose d'écrire des tests pour la méthode  availableTablesForPartySize  de la façade.

Consigne

  1. Créez une suite imbriquée appelée "Available tables".

  2. Repérez les différents comportements pour couvrir tous les happy paths

  3. Testez également le ou les edge case(s)

En résumé

  • le TestBed configure l'environnement de test Angular et gère l'injection de dépendances

  • séparer les tests autour du pivot point des façades simplifie les tests et isole les responsabilités

  • utiliser  provideHttpClientTesting()  permet la gestion des requêtes HTTP avec le  HttpTestingController

  • on teste les méthodes publiques de la façade car ce sont elles qui représentent les comportements, les use cases 

Pour l'instant, vous avez écrit un test pour valider une requête GET. Et si on voulait valider ce que l'application renvoie vers le serveur ? La suite dans le prochain chapitre !

Et si vous obteniez un diplôme OpenClassrooms ?
  • Formations jusqu’à 100 % financées
  • Date de début flexible
  • Projets professionnalisants
  • Mentorat individuel
Trouvez la formation et le financement faits pour vous