Cours NestJS · Module 7: Tests
Performance Testing - les épreuves d'endurance
Dans cette leçon5
La légion a déjà vérifié que les machines fonctionnent. Reste une autre question : combien tiendront-elles ? Une catapulte qui tire un seul coup est inutile dans un siège qui dure une semaine. Les tests de performance mesurent exactement cela - non pas si le code fonctionne, mais combien de temps et sous quelle pression.
Ces épreuves sont toutefois extrêmement sensibles au désordre. Un test qui laisse un millier d'enregistrements en base faussera le résultat du suivant. Avant de mesurer l'endurance des machines, nous allons donc mettre le terrain d'épreuve en ordre - et cet ordre te servira dans tous les tests, pas seulement ceux de performance.
Où vit un test
Commençons par le nom du fichier, car une convention s'applique ici, suivie par tout l'écosystème NestJS :
1legions.service.spec.tsOn le lit de gauche à droite : quoi nous testons (legions), quel type d'élément c'est (service), qu'il s'agit d'un fichier de test (spec), et enfin l'extension (ts). Les tests E2E reçoivent à la place .e2e-spec.ts.
Cette convention n'est pas décorative - Jest recherche les fichiers d'après ce motif par défaut. Un fichier nommé testLegions.ts ne s'exécutera tout simplement jamais, et tu te demanderas pourquoi la garde en CI reste au vert. Nous gardons le fichier à côté du code qu'il teste, ce qui rend visible d'un coup d'œil ce qui a des tests et ce qui n'en a pas.
Le motif AAA - les trois temps d'une épreuve
Le bloc describe qui regroupe les tests d'un même élément s'appelle une suite. À l'intérieur, chaque test suit le même rythme, connu sous le nom d'AAA :
1it('traite 100 requêtes en moins de 2 secondes', async () => {
2 // Arrange - prépare les données et les doublures
3 const legions = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `Legio ${i}` }));
4 jest.spyOn(repo, 'find').mockResolvedValue(legions);
5
6 // Act - appelle la méthode testée
7 const start = Date.now();
8 await Promise.all(Array.from({ length: 100 }, () => service.findAll()));
9 const duration = Date.now() - start;
10
11 // Assert - vérifie le résultat
12 expect(duration).toBeLessThan(2000);
13});Les trois temps correspondent aux trois mots. Arrange prépare la scène : données, doublures, état initial. Act est un appel unique - ce que nous examinons précisément. Assert vérifie le résultat par une ou plusieurs assertions.
L'intérêt de ce découpage est pratique : quand un test échoue, tu sais aussitôt dans quel temps chercher. Et si tu trouves plus d'un appel dans la section Act, c'est le signe que le test examine deux choses à la fois et qu'il vaut mieux le scinder.
Aux trois temps s'ajoute un quatrième, facile à oublier : Cleanup - le rangement après le test, appelé dans le jargon teardown. Nous ne l'écrivons d'ordinaire pas dans le corps du test mais dans un hook séparé, et c'est justement lui qui décide de l'honnêteté des épreuves de performance.
Les hooks - quand préparer, quand ranger
Jest propose quatre hooks, et le choix entre eux tient à une seule question : cela doit-il se produire une fois, ou avant chaque test ?
1describe('LegionsService - performance', () => {
2 let service: LegionsService;
3 let module: TestingModule;
4
5 beforeAll(async () => {
6 module = await Test.createTestingModule({
7 providers: [LegionsService, { provide: getRepositoryToken(Legion), useValue: repo }],
8 }).compile();
9
10 service = module.get(LegionsService);
11 });
12
13 afterEach(() => {
14 jest.clearAllMocks();
15 });
16
17 afterAll(async () => {
18 await module.close();
19 });
20});beforeAll s'exécute une fois avant tous les tests du bloc - nous y construisons le module de test, car c'est une opération coûteuse et rien ne justifie de la répéter cent fois. beforeEach s'exécuterait avant chaque test ; c'est là que va ce qui doit être frais, par exemple le vidage d'une table.
De l'autre côté, afterEach range après chaque test - comme ici la remise à zéro des doublures - et afterAll ferme ce que beforeAll a ouvert : connexions, module, fichiers.
La règle est symétrique et mérite d'être retenue : ce que beforeAll a ouvert, afterAll le ferme ; ce que beforeEach a préparé, afterEach le nettoie. Une connexion non fermée fera se figer Jest après le dernier test, en signalant que quelque chose tourne encore.
Les cas limites (edge cases) - là où les machines cèdent
Un test sous charge typique dit peu de chose. L'intéressant se passe aux extrêmes, et pour les épreuves d'endurance il y en a trois : zéro, un et énormément.
1describe('findAll sous charge', () => {
2 it('renvoie un tableau vide quand il n\'y a aucune légion', async () => {
3 jest.spyOn(repo, 'find').mockResolvedValue([]);
4
5 await expect(service.findAll()).resolves.toEqual([]);
6 });
7
8 it('reste sous 5 secondes avec 10 000 légions', async () => {
9 jest.spyOn(repo, 'find').mockResolvedValue(makeLegions(10_000));
10
11 const start = Date.now();
12 await service.findAll();
13
14 expect(Date.now() - start).toBeLessThan(5000);
15 });
16});Le premier test garde le vide - là où le code aime lever une erreur sur legions[0]. Le second est un spike : une charge soudaine, plusieurs fois supérieure, qui vérifie si la méthode monte en charge raisonnablement ou commence à s'étrangler sur chaque enregistrement pris isolément.
Une remarque sur les seuils de temps : fixe-les avec de la marge. Une machine de CI est souvent plus lente que la tienne, et un test qui échoue au hasard une fois sur dix sera désactivé par la première personne qu'il empêchera de déployer - à juste titre. Un seuil large mais digne de confiance vaut mieux qu'un seuil serré et ignoré.
Récapitulatif
Le terrain d'épreuve est en ordre, les machines sont mesurées :
- les tests de performance demandent non pas si le code fonctionne, mais combien de temps et sous quelle pression,
- le nom du fichier est la convention par laquelle Jest trouve les tests :
nom.service.spec.ts, et.e2e-spec.tspour l'E2E, - le motif AAA :
Arrangeprépare données et doublures,Actappelle la méthode testée,Assertvérifie le résultat, etCleanuprange, - plus d'un appel dans la section
Actsignifie que le test examine deux choses à la fois, - une suite est un bloc
describe; la préparation s'appelle setup, le rangement teardown, beforeAlletafterAllprennent en charge ce qui est coûteux et unique ;beforeEachetafterEachce qui doit être frais à chaque test,- ce que
beforeAlla ouvert,afterAllle ferme - une connexion non fermée fige Jest après le dernier test, - les cas limites de l'endurance sont zéro, un et énormément ; ce dernier s'appelle un spike,
- fixe les seuils de temps avec de la marge - un test qui échoue au hasard finit désactivé et cesse de protéger quoi que ce soit.
Dans la prochaine leçon, ces épreuves cesseront de dépendre de ta mémoire - nous les confierons à une garde automatique en CI. En attendant, retiens ceci : le motif AAA, ce sont les trois temps d'une épreuve, et les hooks veillent à ce que chacune commence sur un terrain propre.
Code de cette leçon : src/performance-testing.ts
1// Performance Testing - Épreuves d'endurance du fort
2
3// Outils pour tester la performance dans NestJS
4
5// 1. Mesurer le temps d'exécution
6class PerformanceTester {
7 static async measureExecution<T>(
8 fn: () => Promise<T>,
9 label: string
10 ): Promise<{ result: T; duration: number }> {
11 const start = performance.now();
12 const result = await fn();
13 const duration = performance.now() - start;
14 console.log(`[PERF] ${label}: ${duration.toFixed(2)}ms`);
15 return { result, duration };
16 }
17
18 // Tester la charge - simuler de nombreux utilisateurs
19 static async loadTest(
20 fn: () => Promise<any>,
21 concurrency: number,
22 iterations: number
23 ) {
24 const results: number[] = [];
25
26 for (let i = 0; i < iterations; i++) {
27 const promises = Array.from({ length: concurrency }, () => {
28 const start = performance.now();
29 return fn().then(() => performance.now() - start);
30 });
31 const times = await Promise.all(promises);
32 results.push(...times);
33 }
34
35 const sorted = [...results].sort((a, b) => a - b);
36 const avg = results.reduce((a, b) => a + b, 0) / results.length;
37
38 return {
39 totalRequests: results.length,
40 avgResponseTime: avg.toFixed(2) + 'ms',
41 maxResponseTime: Math.max(...results).toFixed(2) + 'ms',
42 minResponseTime: Math.min(...results).toFixed(2) + 'ms',
43 p95: sorted[Math.floor(results.length * 0.95)].toFixed(2) + 'ms',
44 };
45 }
46}
47
48// 2. Service avec des opérations de complexité variable
49class LegionQueryService {
50 private legions: Array<{ id: number; name: string; soldiers: number }>;
51
52 constructor() {
53 this.legions = Array.from({ length: 10000 }, (_, i) => ({
54 id: i + 1,
55 name: `Legio ${i + 1}`,
56 soldiers: Math.floor(Math.random() * 10000),
57 }));
58 }
59
60 findByName(name: string) {
61 return this.legions.find(l => l.name === name); // O(n)
62 }
63
64 findById(id: number) {
65 return this.legions[id - 1]; // O(1)
66 }
67
68 getTopLegions(count: number) {
69 return [...this.legions]
70 .sort((a, b) => b.soldiers - a.soldiers)
71 .slice(0, count); // O(n log n)
72 }
73}
74
75// 3. Benchmark
76async function runBenchmark() {
77 const service = new LegionQueryService();
78
79 console.log('=== BENCHMARK : recherche de légions ===');
80
81 await PerformanceTester.measureExecution(
82 async () => service.findById(5000),
83 'findById (O(1))'
84 );
85
86 await PerformanceTester.measureExecution(
87 async () => service.findByName('Legio 5000'),
88 'findByName (O(n))'
89 );
90
91 await PerformanceTester.measureExecution(
92 async () => service.getTopLegions(10),
93 'getTopLegions (O(n log n))'
94 );
95}
96Tu as repéré une erreur dans cette leçon ?
Tâches pratiques dans le jeu
- Éditeur de code
Crée une suite de tests complète avec setup, teardown et cas limites
- Tri horizontal
Ordonne la convention correcte de nommage d'un fichier de test :
- Clique dans l'ordre
Ordonne le pattern de test AAA (Arrange-Act-Assert) :