Cours JavaScript et TypeScript · Module 11: Tests avec Jest
Les matchers dans Jest
Dans cette leçon6
Se demander seulement « est-ce que le résultat est bon ? » ne suffit souvent pas. Parfois, tu veux savoir si la liste des espèces contient un raptor, si la santé reste dans la norme ou si la fonction a déclenché l'alarme. Pour chacune de ces questions, Jest a un outil dédié.
Les matchers sont des méthodes qui permettent de vérifier des valeurs de différentes façons. Dans le contexte de Jurassic Park, ce sont différents types de capteurs, chacun détectant un type de menace particulier. Tu les ajoutes après un point, derrière expect(...), et la règle de choix est simple : prends le capteur le plus précis, car c'est alors que Jest affiche le message d'erreur le plus lisible.
Matchers de comparaison de base
toBe - Comparaison stricte (===)
toBe utilise la comparaison stricte. Il vérifie l'identité - idéal pour les types primitifs. Plus précisément : Jest compare les valeurs avec la fonction Object.is, qui fonctionne comme === à deux exceptions près - NaN est égal à NaN, et 0 et -0 sont distingués :
1test('toBe - comparaison de primitives', () => {
2 const dinoCount = 15;
3 expect(dinoCount).toBe(15); // nombre
4
5 const species = 'T-Rex';
6 expect(species).toBe('T-Rex'); // string
7
8 const isAlive = true;
9 expect(isAlive).toBe(true); // boolean
10});Chacune des trois assertions compare une primitive : un nombre, un texte et une valeur logique. Pour elles, seule la valeur elle-même compte, donc toBe est ici le choix le plus simple.
toEqual - Comparaison structurelle
toEqual vérifie l'égalité structurelle des objets. Il compare le contenu, pas les références. C'est important, car deux objets au contenu identique sont deux spécimens distincts en mémoire : pour toBe, ils seront donc toujours différents :
1test("toEqual - comparaison d'objets", () => {
2 const dino1 = { name: 'Rex', species: 'T-Rex' };
3 const dino2 = { name: 'Rex', species: 'T-Rex' };
4
5 // toBe FAIL - références différentes (objets en mémoire)
6 // expect(dino1).toBe(dino2); // Ça ne marchera pas !
7
8 // toEqual PASS - même contenu
9 expect(dino1).toEqual(dino2); // Ça marche !
10
11 // Fonctionne aussi avec des objets imbriqués
12 const enclosure = {
13 name: 'Zone A',
14 dinosaurs: [
15 { name: 'Rex', diet: 'carnivore' }
16 ]
17 };
18
19 expect(enclosure).toEqual({
20 name: 'Zone A',
21 dinosaurs: [
22 { name: 'Rex', diet: 'carnivore' }
23 ]
24 });
25});toEqual descend récursivement dans les objets et les tableaux imbriqués, champ par champ. Il se montre d'ailleurs indulgent : il ignore les champs qui valent undefined et ne vérifie pas si l'objet est une instance de la même classe. Quand ces détails comptent, utilise toStrictEqual - moi, je le choisis chaque fois que je n'ai aucune raison d'être indulgent.
Matchers pour les tableaux et les chaînes
toContain - Contient-il l'élément ?
Quand toute la liste ne t'intéresse pas, seulement la présence d'un spécimen, utilise toContain. Il fonctionne aussi bien avec les tableaux qu'avec les chaînes :
1test('toContain - vérification du contenu', () => {
2 const dinosaurs = ['T-Rex', 'Velociraptor', 'Triceratops'];
3
4 expect(dinosaurs).toContain('T-Rex');
5 expect(dinosaurs).toContain('Velociraptor');
6
7 // Pour les chaînes
8 const report = 'Dinosaure T-Rex dans la zone A';
9 expect(report).toContain('T-Rex');
10 expect(report).toContain('zone A');
11});Pour un tableau, toContain vérifie si l'un des éléments est strictement égal à celui recherché, et pour une chaîne, si elle contient le fragment indiqué. Attention aux objets : toContain compare les références, donc pour chercher un objet d'après son contenu, on utilise toContainEqual.
toHaveLength - Vérifier la longueur
Le nombre de spécimens dans l'enclos est une autre question fréquente. toHaveLength vérifie la propriété length, il fonctionnera donc avec les tableaux comme avec les chaînes :
1test('toHaveLength - vérification de la quantité', () => {
2 const carnivores = ['T-Rex', 'Velociraptor', 'Spinosaurus'];
3 expect(carnivores).toHaveLength(3);
4
5 const emptyEnclosure = [];
6 expect(emptyEnclosure).toHaveLength(0);
7
8 // Fonctionne aussi avec les chaînes
9 const code = 'DINO-001';
10 expect(code).toHaveLength(8);
11});L'écriture expect(carnivores).toHaveLength(3) donne le même résultat que expect(carnivores.length).toBe(3), mais en cas d'échec, Jest affichera tout le tableau, et pas seulement un nombre. Un tableau vide a une longueur de 0, et c'est aussi une vérification correcte.
Matchers pour les valeurs vraies/fausses
toBeTruthy et toBeFalsy
En JavaScript, toute valeur placée dans une condition if se convertit en true ou en false. toBeTruthy et toBeFalsy vérifient justement le résultat de cette conversion, et non une valeur précise :
1test('vérification truthy et falsy', () => {
2 // Truthy values
3 expect(1).toBeTruthy();
4 expect('text').toBeTruthy();
5 expect([]).toBeTruthy(); // un tableau vide est truthy !
6 expect({}).toBeTruthy(); // un objet vide est truthy !
7
8 // Falsy values
9 expect(0).toBeFalsy();
10 expect('').toBeFalsy();
11 expect(null).toBeFalsy();
12 expect(undefined).toBeFalsy();
13 expect(false).toBeFalsy();
14});Le piège se cache dans le premier groupe : un tableau vide et un objet vide sont truthy. Si tu veux vérifier que l'enclos est vide, toBeFalsy ne t'aidera pas - utilise toHaveLength(0).
toBeNull, toBeUndefined, toBeDefined
On suppose que getDinosaur renvoie un objet dinosaure, ou null quand le spécimen ne figure pas dans le registre. Trois matchers distinguent trois états différents d'absence de valeur :
1test('vérification de null et undefined', () => {
2 const activeDino = getDinosaur('Rex-001');
3 const extinctDino = getDinosaur('Unknown');
4
5 expect(activeDino).toBeDefined();
6 expect(activeDino).not.toBeNull();
7
8 expect(extinctDino).toBeNull();
9 expect(extinctDino).toBeDefined(); // null est defined !
10
11 let uninitializedDino;
12 expect(uninitializedDino).toBeUndefined();
13});toBeDefined signifie seulement « différent de undefined », c'est pourquoi null passe ce test. Si tu veux exclure les deux états vides à la fois, vérifie une valeur précise au lieu de te reposer sur toBeDefined.
Matchers numériques
Les capteurs du parc renvoient des nombres : la santé, la tension de la clôture, la température de l'incubateur. Pour les comparer, on utilise des matchers dont les noms se lisent comme de simples phrases en anglais :
1test('comparaisons numériques', () => {
2 const health = 85;
3
4 expect(health).toBeGreaterThan(80);
5 expect(health).toBeGreaterThanOrEqual(85);
6 expect(health).toBeLessThan(100);
7 expect(health).toBeLessThanOrEqual(85);
8
9 // Pour les nombres à virgule flottante - N'utilise PAS toBe !
10 expect(0.1 + 0.2).toBeCloseTo(0.3);
11});La dernière ligne est une leçon importante sur les nombres à virgule flottante : en JavaScript, 0.1 + 0.2 donne 0.30000000000000004, donc toBe(0.3) ne passerait pas. Par défaut, toBeCloseTo compare avec une précision de deux chiffres après la virgule.
Tester les exceptions - toThrow
toThrow vérifie si une fonction lève une exception. ATTENTION : tu dois envelopper l'appel dans une fonction ! Sans cette enveloppe, l'erreur explosera avant que expect ait le temps de l'attraper. Dans l'exemple, releaseDinosaur ne libère un spécimen qu'avec un niveau de sécurité suffisant :
1function releaseDinosaur(dinosaur, securityLevel) {
2 if (securityLevel < 5) {
3 throw new Error('Niveau de sécurité trop bas !');
4 }
5 if (dinosaur.species === 'T-Rex' && securityLevel < 9) {
6 throw new Error('Le T-Rex exige un niveau de sécurité 9 !');
7 }
8 return { released: true };
9}
10
11test('toThrow - tester les exceptions', () => {
12 const rex = { name: 'Rex', species: 'T-Rex' };
13
14 // CORRECT - enveloppe l'appel dans une fonction fléchée
15 expect(() => releaseDinosaur(rex, 3)).toThrow();
16 expect(() => releaseDinosaur(rex, 3)).toThrow('Niveau de sécurité trop bas !');
17 expect(() => releaseDinosaur(rex, 7)).toThrow('Le T-Rex exige un niveau de sécurité 9 !');
18
19 // Vérifie qu'elle ne lève PAS d'exception
20 expect(() => releaseDinosaur(rex, 9)).not.toThrow();
21});L'argument de toThrow restreint la vérification : une chaîne signifie que le message d'erreur doit contenir ce fragment, une expression régulière, qu'il doit lui correspondre, et une classe d'erreur, que l'exception doit en être une instance. Je te recommande de toujours indiquer le message car toThrow() seul laisse passer n'importe quelle exception, y compris une TypeError causée par une faute de frappe.
Le modificateur not
Chaque matcher peut être inversé avec .not. C'est utile quand tu veux confirmer une absence, par exemple que le stégosaure n'a pas atterri dans l'enclos des prédateurs :
1test('le modificateur not', () => {
2 const dinosaurs = ['T-Rex', 'Triceratops'];
3
4 expect(dinosaurs).not.toContain('Stegosaurus');
5 expect(dinosaurs.length).not.toBe(0);
6 expect(dinosaurs).not.toEqual([]);
7
8 const health = 85;
9 expect(health).not.toBeLessThan(50);
10});not inverse le résultat de n'importe quel matcher, mais n'en abuse pas : not.toBeLessThan(50) se lit plus difficilement que toBeGreaterThanOrEqual(50). Réserve not aux endroits où la phrase « ne contient pas » sonne naturellement.
Dans la leçon sur le mocking, tu découvriras les matchers qui vérifient les appels de fonctions, et avec TypeScript, les matchers asymétriques, qui vérifient uniquement la forme d'un objet. Dans le laboratoire ci-dessous, tu testeras une version simplifiée de expect avec ces mêmes matchers.
Retiens : un matcher est un capteur - choisis celui qui détecte exactement la menace que tu cherches.
Code de cette leçon : index.js
1// Les matchers dans Jest - différents types de vérifications
2console.log("=== Jurassic Park - Capteurs et Matchers ===\n");
3
4// Mini expect avec plusieurs matchers
5let passed = 0, failed = 0;
6
7function expect(actual) {
8 function check(condition, msg) {
9 if (condition) { passed++; console.log(" [PASS] " + msg); }
10 else { failed++; console.log(" [FAIL] " + msg); }
11 }
12
13 return {
14 toBe(expected) {
15 check(actual === expected, `${actual} === ${expected}`);
16 },
17 toEqual(expected) {
18 check(JSON.stringify(actual) === JSON.stringify(expected),
19 `deepEqual : ${JSON.stringify(actual)}`);
20 },
21 toContain(item) {
22 const has = Array.isArray(actual) ? actual.includes(item) : actual.includes(item);
23 check(has, `contient "${item}"`);
24 },
25 toHaveLength(len) {
26 check(actual.length === len, `longueur ${actual.length} === ${len}`);
27 },
28 toBeTruthy() {
29 check(!!actual, `${actual} est truthy`);
30 },
31 toBeFalsy() {
32 check(!actual, `${actual} est falsy`);
33 },
34 toBeGreaterThan(n) {
35 check(actual > n, `${actual} > ${n}`);
36 },
37 toBeLessThan(n) {
38 check(actual < n, `${actual} < ${n}`);
39 },
40 toBeCloseTo(expected) {
41 check(Math.abs(actual - expected) < 0.01, `${actual} ~= ${expected}`);
42 },
43 toBeNull() {
44 check(actual === null, `${actual} est null`);
45 },
46 toBeUndefined() {
47 check(actual === undefined, `est undefined`);
48 },
49 toBeDefined() {
50 check(actual !== undefined, `est défini`);
51 },
52 not: {
53 toBe(expected) { check(actual !== expected, `${actual} !== ${expected}`); },
54 toContain(item) {
55 const has = Array.isArray(actual) ? actual.includes(item) : actual.includes(item);
56 check(!has, `ne contient pas "${item}"`);
57 },
58 toThrow() { /* simplifié */ }
59 }
60 };
61}
62
63// --- toBe vs toEqual ---
64console.log("--- toBe vs toEqual ---");
65
66const dino1 = { name: "Rex", species: "T-Rex" };
67const dino2 = { name: "Rex", species: "T-Rex" };
68
69console.log("toBe compare les références (===) :");
70console.log(" dino1 === dino2 ?", dino1 === dino2, "(false - objets différents)");
71console.log("\ntoEqual compare le contenu :");
72expect(dino1).toEqual(dino2);
73
74// --- toContain ---
75console.log("\n--- toContain ---");
76const dinosaurs = ["T-Rex", "Velociraptor", "Triceratops"];
77expect(dinosaurs).toContain("T-Rex");
78expect(dinosaurs).not.toContain("Stegosaurus");
79
80const report = "Dinosaure T-Rex dans la zone A";
81expect(report).toContain("T-Rex");
82
83// --- toHaveLength ---
84console.log("\n--- toHaveLength ---");
85const carnivores = ["T-Rex", "Velociraptor", "Spinosaurus"];
86expect(carnivores).toHaveLength(3);
87
88// --- Truthy / Falsy ---
89console.log("\n--- Truthy / Falsy ---");
90expect(1).toBeTruthy();
91expect("text").toBeTruthy();
92expect(0).toBeFalsy();
93expect("").toBeFalsy();
94expect(null).toBeFalsy();
95
96// --- Nombres ---
97console.log("\n--- Comparaisons numériques ---");
98const health = 85;
99expect(health).toBeGreaterThan(80);
100expect(health).toBeLessThan(100);
101expect(0.1 + 0.2).toBeCloseTo(0.3);
102
103// --- null / undefined ---
104console.log("\n--- null / undefined ---");
105expect(null).toBeNull();
106expect(undefined).toBeUndefined();
107expect("value").toBeDefined();
108
109// --- toThrow ---
110console.log("\n--- toThrow ---");
111function releaseDinosaur(dino, level) {
112 if (level < 5) throw new Error("Niveau trop bas !");
113 return { released: true };
114}
115
116try {
117 releaseDinosaur({}, 3);
118 console.log(" [FAIL] Aurait dû lever une exception");
119 failed++;
120} catch (e) {
121 console.log(" [PASS] Exception levée : " + e.message);
122 passed++;
123}
124
125try {
126 releaseDinosaur({}, 9);
127 console.log(" [PASS] Pas d'exception pour le niveau 9");
128 passed++;
129} catch (e) {
130 console.log(" [FAIL] N'aurait pas dû lever d'exception");
131 failed++;
132}
133
134console.log(`\n=== Résultats : ${passed} réussis, ${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. Quelle est la principale différence entre les matchers toBe et toEqual dans Jest ?
2. Comment utiliser correctement le matcher toThrow dans Jest ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Pour chaque test, utilise le bon matcher : toBe, toContain, toHaveLength, toBeGreaterThan, toEqual.
- Tri horizontal
Ordonne les éléments d'une assertion comparant un objet :
- Éditeur de code
Écris des tests vérifiant à la fois des conditions positives et négatives.