
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.
Regardons cet écran :

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 !
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 ?!
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.
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 !
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/tablesC'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 !
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
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 !

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.
Créez une suite imbriquée appelée "Available tables".
Repérez les différents comportements pour couvrir tous les happy paths
Testez également le ou les edge case(s)
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 !