Cours NestJS · Module 7: Tests
Integration Testing - vérifier que la légion coopère
Dans cette leçon6
Chaque pièce de la catapulte a passé l'inspection. La corde tient, le mécanisme se déclenche, le contrepoids pèse exactement ce qu'il faut. Tu assembles la machine - et le projectile vole trois pas. Pourquoi ? Parce que la corde s'est révélée trop courte d'un mètre pour ce bras. Chaque pièce était bonne isolément, mais personne n'a vérifié si elles s'accordaient.
C'est la faille qu'un test unitaire ne peut par construction pas voir - nous y avons remplacé chaque pièce voisine par une doublure. Un test d'intégration comble cette faille : il assemble plusieurs éléments réels et vérifie s'ils se parlent comme nous l'avions supposé.
La pyramide des tests - combien écrire de quoi
Avant d'écrire un tel test, situons-le parmi les autres. Les tests forment une pyramide - les plus nombreux en bas, les plus rares en haut :
- Unit - la base. Les plus nombreux et les plus rapides, car ils ne touchent rien au-delà d'une classe.
- Integration - la couche intermédiaire. Moins nombreux, plus lents, car ils font tourner plusieurs éléments à la fois.
- E2E - le sommet. Les moins nombreux et les plus lents ; ils parcourent toute la route d'une requête.
- Tests manuels - sporadiques, là où l'automatisation n'atteint pas.
La forme de la pyramide n'est pas arbitraire : plus on monte, plus un test donne de certitude, mais plus il coûte de temps et plus il se casse pour des raisons futiles. Une pyramide inversée - une poignée de tests unitaires et des centaines d'E2E - donne une suite qui tourne un quart d'heure et échoue dès qu'un libellé de bouton change.
Le même ordre régit leur exécution : npm run test (unitaires, quelques secondes) → test:watch (les mêmes, en boucle pendant que tu écris) → test:cov (unitaires plus calcul de couverture) → test:e2e (les plus lents, en dernier).
Unit contre integration - une seule différence
La distinction tient à une seule question : combien d'éléments réels participent au test ?
Un test unitaire examine un composant et remplace toutes ses dépendances par des doublures. Un test d'intégration examine plusieurs qui coopèrent - un contrôleur avec son service, un service avec son repository.
Cela ne signifie pas que les doublures disparaissent dans les tests d'intégration. Tu remplaces toujours ce qui se trouve au-delà de la frontière de la portion testée : paiements, envoi d'e-mails, API d'autrui. Tu déplaces seulement la frontière - d'une classe vers plusieurs qui coopèrent.
Assembler un module à partir de pièces réelles
Un test d'intégration construit un module presque comme un test unitaire, mais au lieu d'énumérer des providers isolés, il importe le module applicatif entier :
1const module = await Test.createTestingModule({
2 imports: [
3 TypeOrmModule.forRoot({
4 type: 'sqlite',
5 database: ':memory:',
6 entities: [Legion],
7 synchronize: true,
8 }),
9 LegionsModule,
10 ],
11}).compile();LegionsModule entre en entier - avec son contrôleur, son service et son repository, exactement comme dans la vraie application. La nouveauté est la configuration de la base : type: 'sqlite' avec database: ':memory:' crée une base n'existant que dans la mémoire du processus. Elle naît au démarrage du test et disparaît à sa fin, tu n'as donc besoin d'aucun serveur, et chaque exécution part d'une table vide.
synchronize: true demande à TypeORM de construire les tables directement d'après les entités. Dans une application de production, cette option est interdite - les migrations y font loi - mais dans une base qui vit trois secondes, c'est exactement ce qu'il faut.
Tester par du vrai HTTP
Puisque le module contient un contrôleur, nous pouvons y frapper comme le ferait un client - par une requête HTTP :
1describe('LegionsController (intégration)', () => {
2 let app: INestApplication;
3
4 beforeAll(async () => {
5 const module = await Test.createTestingModule({
6 imports: [/* ... */],
7 }).compile();
8
9 app = module.createNestApplication();
10 await app.init();
11 });
12
13 afterAll(async () => {
14 await app.close();
15 });
16
17 it('POST /legions enregistre une légion et la renvoie avec un id', async () => {
18 const response = await request(app.getHttpServer())
19 .post('/legions')
20 .send({ name: 'Legio X' })
21 .expect(201);
22
23 expect(response.body.id).toBeDefined();
24 expect(response.body.name).toBe('Legio X');
25 });
26});Trois éléments nouveaux méritent d'être nommés. module.createNestApplication() transforme le module compilé en application en fonctionnement - capable de traiter une requête. app.init() la démarre ; sans cet appel, le serveur n'acceptera rien.
Ensuite request(app.getHttpServer()) - c'est la bibliothèque supertest. getHttpServer() extrait le serveur HTTP de l'application, et request(...) lui envoie une vraie requête sans ouvrir de port. Le trafic se passe en mémoire, le test n'occupe donc pas le réseau et n'entrera pas en collision avec un autre processus.
La chaîne se lit comme la description de la requête : .post('/legions') choisit la méthode et l'adresse, .send({...}) ajoute le corps, .expect(201) vérifie le code de réponse. Le response.body renvoyé s'examine ensuite par des assertions ordinaires.
Et l'on voit ici ce que ce test apporte de plus qu'un test unitaire : le projectile a parcouru toute la route - routage, validation du corps, service, repository, jusqu'à la base et retour. Si le contrôleur attendait un champ title alors que le service enregistre name, les tests unitaires des deux classes passeraient sans broncher ; celui-ci échoue.
Ranger entre les tests
Une vraie base, fût-elle en mémoire, retient tout ce qu'on y a écrit - y compris lors du test précédent :
1afterEach(async () => {
2 await app.get(getRepositoryToken(Legion)).clear();
3});clear() vide la table après chaque test. Sans cela, un test qui compte les légions verra aussi celles créées auparavant et se mettra à dépendre de l'ordre d'exécution - exactement le piège que beforeEach désamorçait dans les tests unitaires.
Note le choix des hooks : nous construisons l'application dans beforeAll (une fois, car c'est coûteux) et vidons les données dans afterEach (à chaque fois, car c'est bon marché). La fermeture par app.close() dans afterAll est obligatoire - sinon Jest se fige avec une connexion ouverte à la base.
Récapitulatif
La machine est assemblée et tirée - pour de vrai cette fois :
- un test d'intégration vérifie la coopération de plusieurs composants, là où un test unitaire en examine un en isolation,
- les doublures ne disparaissent pas - seule la frontière où on les place se déplace,
- la pyramide des tests : beaucoup d'unitaires, moins d'intégration, très peu d'E2E, quelques manuels,
- l'ordre du plus rapide :
test,test:watch,test:cov,test:e2e, - nous assemblons le module à partir de pièces réelles par
imports: [Module], en remplaçant la base par sqlite:memory:avecsynchronize: true, createNestApplication()transforme un module en application en fonctionnement,app.init()la démarre,request(app.getHttpServer())de la bibliothèque supertest envoie une vraie requête sans ouvrir de port,- la chaîne
.post().send().expect(201)décrit la requête, et tu examinesresponse.bodypar des assertions ordinaires, clear()dansafterEachvide la table,app.close()dansafterAllferme l'application - sans quoi Jest se fige.
Dans la prochaine leçon, nous monterons au sommet même de la pyramide - vers les tests E2E, qui parcourent toute la route d'un utilisateur. En attendant, retiens ceci : un test unitaire dit que chaque pièce fonctionne ; un test d'intégration dit qu'elles s'accordent.
Code de cette leçon : src/integration-testing.spec.ts
1// Integration Testing - Vérifier la coopération de la légion
2import { Test, TestingModule } from '@nestjs/testing';
3import { Injectable } from '@nestjs/common';
4
5// Service d'approvisionnement
6@Injectable()
7class SupplyService {
8 calculateSupplies(soldiers: number): number {
9 // Chaque soldat a besoin de 3 unités d'approvisionnement
10 return soldiers * 3;
11 }
12
13 checkSupplyStatus(current: number, required: number) {
14 return {
15 sufficient: current >= required,
16 shortage: Math.max(0, required - current),
17 };
18 }
19}
20
21// Service de garnison - dépend de SupplyService
22@Injectable()
23class GarrisonService {
24 constructor(private supplyService: SupplyService) {}
25
26 assessReadiness(soldiers: number, currentSupplies: number) {
27 const requiredSupplies = this.supplyService.calculateSupplies(soldiers);
28 const status = this.supplyService.checkSupplyStatus(
29 currentSupplies, requiredSupplies
30 );
31
32 return {
33 soldiers,
34 requiredSupplies,
35 currentSupplies,
36 ready: status.sufficient,
37 shortage: status.shortage,
38 };
39 }
40}
41
42// Test d'intégration - vérifie la coopération des services
43describe('Garrison Integration Test', () => {
44 let garrisonService: GarrisonService;
45 let supplyService: SupplyService;
46
47 beforeEach(async () => {
48 // On crée un VRAI module avec les deux services
49 const modulee: TestingModule = await Test.createTestingModule({
50 providers: [GarrisonService, SupplyService],
51 }).compile();
52
53 garrisonService = modulee.get(GarrisonService);
54 supplyService = modulee.get(SupplyService);
55 });
56
57 it('should assess garrison as ready when supplies sufficient', () => {
58 const result = garrisonService.assessReadiness(100, 400);
59
60 expect(result.ready).toBe(true);
61 expect(result.shortage).toBe(0);
62 expect(result.requiredSupplies).toBe(300);
63 });
64
65 it('should detect supply shortage', () => {
66 const result = garrisonService.assessReadiness(100, 200);
67
68 expect(result.ready).toBe(false);
69 expect(result.shortage).toBe(100); // 300 - 200
70 });
71
72 it('should correctly integrate supply calculation', () => {
73 // On vérifie le flux de données entre les services
74 const supplies = supplyService.calculateSupplies(50);
75 expect(supplies).toBe(150);
76
77 const readiness = garrisonService.assessReadiness(50, supplies);
78 expect(readiness.ready).toBe(true);
79 });
80});
81Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Les tests d'intégration dans NestJS vérifient :
2. La principale différence entre les tests unitaires et d'intégration est :
Tâches pratiques dans le jeu
- Éditeur de code
Crée un test d'intégration pour le module des légions
- Tri vertical
Ordonne la pyramide des tests du plus nombreux (bas) au moins nombreux (haut) :
- Clique dans l'ordre
Ordonne les commandes d'exécution des tests du plus rapide :