Cours JavaScript et TypeScript · Module 11: Tests avec Jest
Le framework Jest - Les bases
Dans cette leçon5
Dans la leçon précédente, le test est apparu comme un capteur tout prêt. Mais qui le déclenche, collecte les résultats et donne l'alerte quand quelque chose tourne mal ? Il te faut un centre de contrôle qui trouve tout seul les fichiers de tests, les exécute et rapporte ce qui est passé et ce qui a échoué.
Jest fait partie des outils les plus populaires pour tester JavaScript et TypeScript. Il est né chez Facebook (aujourd'hui Meta), et depuis 2022, c'est l'OpenJS Foundation qui le développe. Dans notre Jurassic Park, Jest sera le principal outil de vérification des systèmes de sécurité - dans ce module, on s'appuie sur sa version 30.
Installation et configuration
Jest s'installe comme dépendance de développement : il atterrit dans la section devDependencies du fichier package.json, car tu en as besoin pendant que tu travailles sur le code, et non en production. Voici les commandes par lesquelles commence chaque projet :
1# Installation de Jest
2npm install --save-dev jest
3
4# Pour TypeScript
5npm install --save-dev jest ts-jest @types/jest
6
7# Initialisation de la configuration
8npm init jest@latestLa première commande suffit pour tester du JavaScript, la deuxième ajoute les paquets pour TypeScript - on y reviendra dans la leçon sur les tests TS. La dernière lance un assistant qui pose quelques questions et enregistre le fichier de configuration. Dans les anciennes versions, on utilisait pour cela la commande npx jest --init, mais Jest 30 l'a supprimée. Pense aussi à ajouter dans package.json le script "test": "jest" : tu pourras alors lancer toute la batterie de tests avec npm test.
Structure d'un test
Chaque test dans Jest se compose de trois éléments clés : describe, it (ou test) et expect. Tu n'as pas besoin de les importer, car Jest les fournit comme fonctions globales dans chaque fichier de test. Les fichiers eux-mêmes, il les trouve aussi sans ton aide : par défaut, il exécute tout ce qui se termine par .test.js ou .spec.js (y compris dans les variantes TypeScript), ainsi que les fichiers du dossier __tests__.
describe - Regrouper les tests
describe est un bloc qui regroupe des tests liés. Vois-le comme un enclos du parc : il regroupe les dinosaures d'une même espèce. Le premier argument est le nom du groupe, le second une fonction dans laquelle tu places les tests :
1describe('DinosaurEnclosure', () => {
2 // Tous les tests liés à l'enclos des dinosaures
3});describe à lui seul ne vérifie rien. Il met de l'ordre dans le rapport : dans les résultats, tu verras le nom DinosaurEnclosure et, en dessous, tous les tests de cet enclos. Les blocs peuvent s'imbriquer, comme tu le verras dans un instant dans l'exemple complet.
it / test - Un cas de test individuel
it et test sont interchangeables : tous deux définissent un test unique. La documentation de Jest décrit explicitement it comme un alias de test : c'est la même fonction sous deux noms. Convention : it se lit comme une phrase en anglais. Dans un même describe, tu peux même mélanger les deux formes :
1describe('DinosaurEnclosure', () => {
2 it('should have a fence', () => {
3 // vérification
4 });
5
6 test('fence is electrified', () => {
7 // vérification
8 });
9});Les deux tests fonctionnent de manière identique. Le premier se lit comme la phrase « it should have a fence », c'est-à-dire « il devrait avoir une clôture ». Je te recommande it à l'intérieur d'un describe, et test pour les tests isolés, mais le plus important reste la cohérence dans tout le projet.
expect - Les assertions
expect est le cœur de chaque test. Il compare le résultat réel au résultat attendu. L'écriture comporte toujours deux parties : expect(valeur) enveloppe le résultat, et le matcher, par exemple toBe ou toEqual, dit ce que tu attends. createEnclosure est une fabrique qui renvoie un objet enclos avec un nom, une capacité et une liste de dinosaures vide :
1describe('DinosaurEnclosure', () => {
2 it('should have correct capacity', () => {
3 const enclosure = createEnclosure('T-Rex Zone', 5);
4 expect(enclosure.capacity).toBe(5);
5 });
6
7 it('should start empty', () => {
8 const enclosure = createEnclosure('Raptor Pen', 10);
9 expect(enclosure.dinosaurs).toEqual([]);
10 expect(enclosure.dinosaurs.length).toBe(0);
11 });
12});Quand une assertion échoue, Jest interrompt uniquement ce test-là, affiche la valeur attendue (Expected) et la valeur reçue (Received), puis passe au suivant. Pourquoi on a utilisé toEqual et non toBe pour le tableau vide, je te l'expliquerai dans la leçon sur les matchers.
Exemple complet
Voyons un exemple complet de test d'un module de gestion des dinosaures. D'abord le code de production : createDinosaur crée un spécimen en pleine santé, et feedDinosaur n'augmente la santé que si la nourriture correspond au régime alimentaire, et jamais au-delà de 100 :
1// dinosaur.js - code de production
2function createDinosaur(name, species, diet) {
3 return {
4 name,
5 species,
6 diet,
7 health: 100,
8 isAlive: true,
9 };
10}
11
12function feedDinosaur(dinosaur, food) {
13 if (dinosaur.diet === 'carnivore' && food === 'meat') {
14 dinosaur.health = Math.min(100, dinosaur.health + 20);
15 return { success: true, message: 'Dinosaure nourri !' };
16 }
17 if (dinosaur.diet === 'herbivore' && food === 'plants') {
18 dinosaur.health = Math.min(100, dinosaur.health + 15);
19 return { success: true, message: 'Dinosaure nourri !' };
20 }
21 return { success: false, message: 'Mauvaise nourriture !' };
22}Passons au fichier de tests. Les deux describe imbriqués correspondent aux deux fonctions, et chaque it vérifie un seul scénario :
1// dinosaur.test.js - tests
2describe('Dinosaur Management', () => {
3 describe('createDinosaur', () => {
4 it('should create a dinosaur with correct properties', () => {
5 const rex = createDinosaur('Rex', 'T-Rex', 'carnivore');
6
7 expect(rex.name).toBe('Rex');
8 expect(rex.species).toBe('T-Rex');
9 expect(rex.diet).toBe('carnivore');
10 expect(rex.health).toBe(100);
11 expect(rex.isAlive).toBe(true);
12 });
13 });
14
15 describe('feedDinosaur', () => {
16 it('should feed carnivore with meat', () => {
17 const rex = createDinosaur('Rex', 'T-Rex', 'carnivore');
18 rex.health = 70;
19
20 const result = feedDinosaur(rex, 'meat');
21
22 expect(result.success).toBe(true);
23 expect(rex.health).toBe(90);
24 });
25
26 it('should reject wrong food for carnivore', () => {
27 const rex = createDinosaur('Rex', 'T-Rex', 'carnivore');
28
29 const result = feedDinosaur(rex, 'plants');
30
31 expect(result.success).toBe(false);
32 });
33 });
34});Dans un vrai projet, le fichier de tests importe les fonctions, par exemple avec require('./dinosaur'), et dinosaur.js doit les exporter. Remarque aussi une lacune : le test de la nourriture refusée ne vérifie pas que la santé de Rex est restée intacte. C'est un bon candidat pour ton premier test écrit en solo.
Lancer les tests
Les tests se lancent depuis le terminal. Voici les quatre variantes que tu utiliseras le plus souvent :
1# Lancer tous les tests
2npx jest
3
4# Lancer un fichier précis
5npx jest dinosaur.test.js
6
7# Lancer en mode watch (réagit aux modifications)
8npx jest --watch
9
10# Lancer avec un rapport de couverture de code
11npx jest --coverageAprès chaque enregistrement, le mode --watch relance uniquement les tests liés aux fichiers modifiés (il détecte les changements grâce à Git), et --coverage crée un rapport de couverture : il montre quelles lignes de code au moins un test a exécutées, et lesquelles aucun test n'a visitées.
Le pattern AAA (Arrange-Act-Assert)
Tout bon test devrait être écrit selon le pattern AAA : Arrange (préparer), Act (exécuter), Assert (vérifier). Trois parties séparées par une ligne vide, et le test se lit comme un rapport d'inspection :
1it('should increase health when fed correctly', () => {
2 // Arrange (Préparation) - crée les objets nécessaires
3 const dino = createDinosaur('Brachio', 'Brachiosaurus', 'herbivore');
4 dino.health = 60;
5
6 // Act (Action) - exécute l'opération testée
7 const result = feedDinosaur(dino, 'plants');
8
9 // Assert (Vérification) - vérifie le résultat
10 expect(result.success).toBe(true);
11 expect(dino.health).toBe(75);
12});Le code de production n'a pas changé - seule l'organisation du test a changé, et grâce à elle on voit tout de suite ce qui relève de la préparation et ce qui est la vérification proprement dite. Mon conseil : une seule action dans la section Act. Si tu y appelles trois fonctions différentes, tu es probablement en train d'écrire trois tests à la fois.
Dans la leçon suivante, tu découvriras tout l'arsenal des matchers. Dans le laboratoire ci-dessous tourne une simulation simplifiée de describe, it et expect écrite en JavaScript pur, car Sandpack n'exécute pas le vrai Jest.
Retiens : describe est l'enclos, it une inspection individuelle, et expect le capteur qui rapporte PASS ou FAIL.
Code de cette leçon : index.js
1// Jest - Les bases du framework de test
2console.log("=== Jurassic Park - Système de Test Jest ===\n");
3
4// Simulation du framework Jest (Sandpack ne lance pas un vrai Jest)
5// Démonstration de la structure : describe, it/test, expect
6
7// --- Mini implémentation de Jest ---
8const results = { passed: 0, failed: 0, suites: [] };
9
10function describe(name, fn) {
11 const suite = { name, tests: [] };
12 results.suites.push(suite);
13 const prevSuite = results.currentSuite;
14 results.currentSuite = suite;
15 fn();
16 results.currentSuite = prevSuite;
17}
18
19function it(name, fn) {
20 try {
21 fn();
22 results.currentSuite.tests.push({ name, status: "PASS" });
23 results.passed++;
24 } catch (e) {
25 results.currentSuite.tests.push({ name, status: "FAIL", error: e.message });
26 results.failed++;
27 }
28}
29const test = it; // test et it sont interchangeables
30
31function expect(actual) {
32 return {
33 toBe(expected) {
34 if (actual !== expected)
35 throw new Error(`Attendu ${expected}, obtenu ${actual}`);
36 },
37 toEqual(expected) {
38 if (JSON.stringify(actual) !== JSON.stringify(expected))
39 throw new Error(`Attendu ${JSON.stringify(expected)}, obtenu ${JSON.stringify(actual)}`);
40 },
41 toHaveLength(expected) {
42 if (actual.length !== expected)
43 throw new Error(`Longueur attendue ${expected}, obtenue ${actual.length}`);
44 },
45 toBeTruthy() {
46 if (!actual) throw new Error(`Attendu truthy, obtenu ${actual}`);
47 },
48 not: {
49 toBe(expected) {
50 if (actual === expected) throw new Error(`Attendu différent de ${expected}`);
51 }
52 }
53 };
54}
55
56// --- Code de production ---
57function createDinosaur(name, species, diet) {
58 return { name, species, diet, health: 100, isAlive: true };
59}
60
61function feedDinosaur(dinosaur, food) {
62 if (dinosaur.diet === "carnivore" && food === "meat") {
63 dinosaur.health = Math.min(100, dinosaur.health + 20);
64 return { success: true, message: "Dinosaure nourri !" };
65 }
66 if (dinosaur.diet === "herbivore" && food === "plants") {
67 dinosaur.health = Math.min(100, dinosaur.health + 15);
68 return { success: true, message: "Dinosaure nourri !" };
69 }
70 return { success: false, message: "Mauvaise nourriture !" };
71}
72
73// --- Tests ---
74describe("Dinosaur Management", () => {
75 describe("createDinosaur", () => {
76 it("should create a dinosaur with correct properties", () => {
77 const rex = createDinosaur("Rex", "T-Rex", "carnivore");
78 expect(rex.name).toBe("Rex");
79 expect(rex.species).toBe("T-Rex");
80 expect(rex.diet).toBe("carnivore");
81 expect(rex.health).toBe(100);
82 expect(rex.isAlive).toBe(true);
83 });
84
85 it("should create herbivore dinosaur", () => {
86 const brachio = createDinosaur("Brachio", "Brachiosaurus", "herbivore");
87 expect(brachio.diet).toBe("herbivore");
88 expect(brachio.health).toBe(100);
89 });
90 });
91
92 describe("feedDinosaur", () => {
93 it("should feed carnivore with meat (AAA pattern)", () => {
94 // Arrange
95 const rex = createDinosaur("Rex", "T-Rex", "carnivore");
96 rex.health = 70;
97
98 // Act
99 const result = feedDinosaur(rex, "meat");
100
101 // Assert
102 expect(result.success).toBe(true);
103 expect(rex.health).toBe(90);
104 });
105
106 it("should reject wrong food for carnivore", () => {
107 const rex = createDinosaur("Rex", "T-Rex", "carnivore");
108 const result = feedDinosaur(rex, "plants");
109 expect(result.success).toBe(false);
110 });
111
112 it("should feed herbivore with plants", () => {
113 const brachio = createDinosaur("Brachio", "Brachiosaurus", "herbivore");
114 brachio.health = 50;
115 const result = feedDinosaur(brachio, "plants");
116 expect(result.success).toBe(true);
117 expect(brachio.health).toBe(65);
118 });
119 });
120});
121
122// --- Afficher les résultats ---
123console.log("Résultats des tests :\n");
124results.suites.forEach(suite => {
125 console.log(` ${suite.name}`);
126 suite.tests.forEach(t => {
127 const icon = t.status === "PASS" ? "PASS" : "FAIL";
128 console.log(` [${icon}] ${t.name}`);
129 if (t.error) console.log(` ${t.error}`);
130 });
131});
132console.log(`\nTotal : ${results.passed} réussis, ${results.failed} échoués`);Tu 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. Que signifie le pattern AAA utilisé dans les tests unitaires ?
2. Quel est le but de la fonction expect() dans Jest ?
Tâches pratiques dans le jeu
- Éditeur de code
Complète les assertions manquantes dans les tests de la fonction getDinosaurSpecies.
- Clique dans l'ordre
Ordonne les éléments d'un test Jest de base :
- Éditeur de code
Écris des tests pour un système de points de vie de dinosaure en utilisant describe, it, expect.