Cours JavaScript et TypeScript · Module 11: Tests avec Jest

Setup et Teardown

6 min de lecture
Dans cette leçon5

Imagine trois tests qui utilisent un seul enclos commun. Le premier y fait entrer un T-Rex, le deuxième vérifie que l'enclos est vide - et il échoue, parce que le T-Rex y est toujours. Des tests qui partagent un état finissent par dépendre les uns des autres : le résultat dépend de l'ordre d'exécution, et une erreur dans l'un fait tomber les suivants comme des dominos.

À Jurassic Park, avant chaque inspection d'enclos, il faut préparer le matériel, et après l'inspection, faire le ménage. Dans les tests, c'est exactement pareil : les fonctions de setup et de teardown permettent de préparer puis de nettoyer l'environnement de test. Dans Jest, on les appelle des hooks, et il y en a quatre : beforeEach, afterEach, beforeAll et afterAll.

beforeEach - Avant chaque test

beforeEach s'exécute avant chaque test du bloc describe. C'est l'endroit idéal pour créer des données de test fraîches. Tu déclares les variables avec let dans la portée du describe, et tu leur affectes les valeurs dans le hook. Les fabriques createEnclosure et createDinosaur, tu les connais déjà grâce aux leçons précédentes :

1describe('DinosaurEnclosure', () => {
2  let enclosure;
3  let rex;
4
5  beforeEach(() => {
6    // Avant chaque test, on crée un nouvel enclos et un nouveau dinosaure
7    enclosure = createEnclosure('Zone A', 5);
8    rex = createDinosaur('Rex', 'T-Rex', 'carnivore');
9  });
10
11  it('should add dinosaur to enclosure', () => {
12    enclosure.add(rex);
13    expect(enclosure.dinosaurs).toHaveLength(1);
14    expect(enclosure.dinosaurs[0].name).toBe('Rex');
15  });
16
17  it('should start with empty enclosure', () => {
18    // Grâce à beforeEach, enclosure est toujours tout neuf !
19    expect(enclosure.dinosaurs).toHaveLength(0);
20  });
21
22  it('should track capacity', () => {
23    enclosure.add(rex);
24    expect(enclosure.remainingCapacity()).toBe(4);
25  });
26});

Le deuxième test passe, bien que le premier ait ajouté Rex à l'enclos. L'ancien objet n'a pas disparu par magie : beforeEach a simplement affecté à la variable enclosure un enclos entièrement neuf. C'est la section Arrange du pattern AAA, déplacée dans un hook pour ne pas la répéter dans chaque test.

afterEach - Après chaque test

afterEach s'exécute après chaque test. Il sert à nettoyer derrière les tests - fermer les connexions, réinitialiser l'état. Dans l'exemple, SecuritySystem est la classe du système de sécurité : onAlarm enregistre la fonction appelée en cas d'alarme, simulateBreach simule une brèche dans la clôture, et deactivate éteint le système :

1describe('SecuritySystem', () => {
2  let securitySystem;
3  let alarmLog;
4
5  beforeEach(() => {
6    securitySystem = new SecuritySystem();
7    alarmLog = [];
8    securitySystem.onAlarm((msg) => alarmLog.push(msg));
9  });
10
11  afterEach(() => {
12    // Réinitialise le système de sécurité après chaque test
13    securitySystem.deactivate();
14    alarmLog = [];
15    console.log('Système réinitialisé après le test');
16  });
17
18  it('should detect breach', () => {
19    securitySystem.simulateBreach('Zone A');
20    expect(alarmLog).toContain('Intrusion en Zone A');
21  });
22
23  it('should handle multiple alarms', () => {
24    securitySystem.simulateBreach('Zone A');
25    securitySystem.simulateBreach('Zone B');
26    expect(alarmLog).toHaveLength(2);
27  });
28});

afterEach s'exécute même quand le test échoue, c'est pourquoi c'est le meilleur endroit pour l'étape de nettoyage (cleanup). La ligne avec console.log montre seulement le moment de l'exécution - dans de vrais tests, elle n'est pas nécessaire.

beforeAll et afterAll - Une fois pour tout le groupe

beforeAll s'exécute une fois avant tous les tests du describe, et afterAll une fois après tous. Utilise-les pour les opérations coûteuses. connectToDatabase est un exemple de fonction asynchrone qui se connecte à la base de données. Les hooks, comme les tests, peuvent être async, et Jest attendra qu'ils se terminent :

1describe('DatabaseTests', () => {
2  let database;
3
4  beforeAll(async () => {
5    // Opération coûteuse - connexion à la base, une seule fois !
6    database = await connectToDatabase();
7    console.log('Base de données connectée');
8  });
9
10  afterAll(async () => {
11    // Ferme la connexion après tous les tests
12    await database.close();
13    console.log('Base de données déconnectée');
14  });
15
16  beforeEach(async () => {
17    // Vide les données avant chaque test
18    await database.clear();
19  });
20
21  it('should save dinosaur', async () => {
22    await database.save({ name: 'Rex', species: 'T-Rex' });
23    const dinos = await database.findAll();
24    expect(dinos).toHaveLength(1);
25  });
26
27  it('should find by species', async () => {
28    await database.save({ name: 'Rex', species: 'T-Rex' });
29    await database.save({ name: 'Blue', species: 'Velociraptor' });
30
31    const raptors = await database.findBySpecies('Velociraptor');
32    expect(raptors).toHaveLength(1);
33    expect(raptors[0].name).toBe('Blue');
34  });
35});

La connexion n'est créée qu'une seule fois, mais on vide les données avant chaque test dans beforeEach. C'est un duo typique : une ressource chère partagée, un état bon marché rafraîchi. Mon conseil : tout ce qui peut être recréé rapidement, crée-le dans beforeEach, et garde beforeAll pour les opérations vraiment coûteuses. L'état partagé est la source la plus fréquente de tests qui ne passent que dans un ordre précis.

Comment reconnaître une telle fuite ? Le test passe quand tu le lances seul, mais échoue dans la suite complète. La documentation de Jest conseille alors de remplacer temporairement test par test.only (ou it par it.only), pour n'exécuter que ce cas-là, puis de chercher l'état qu'un autre test modifie.

Ordre d'exécution

Il est important de comprendre dans quel ordre Jest exécute le setup et le teardown. Le bloc ci-dessous affiche dans la console le numéro de l'étape, depuis chaque hook et chaque test :

1describe("Ordre d'exécution", () => {
2  beforeAll(() => console.log('1. beforeAll'));
3  afterAll(() => console.log('5. afterAll'));
4  beforeEach(() => console.log('2. beforeEach'));
5  afterEach(() => console.log('4. afterEach'));
6
7  it('test 1', () => console.log('3. test 1'));
8  it('test 2', () => console.log('3. test 2'));
9});
10
11// Résultat :
12// 1. beforeAll
13// 2. beforeEach
14// 3. test 1
15// 4. afterEach
16// 2. beforeEach
17// 3. test 2
18// 4. afterEach
19// 5. afterAll

beforeAll et afterAll n'apparaissent qu'une fois dans le résultat, alors que la paire beforeEach et afterEach entoure chaque test séparément. Jest a encore une particularité bonne à connaître : il exécute d'abord toutes les fonctions describe du fichier pour collecter la liste des tests, et ce n'est qu'ensuite qu'il lance les tests eux-mêmes. Du code écrit directement dans un describe, en dehors d'un hook, s'exécutera donc plus tôt que tu ne le penses.

Les describe imbriqués

Le setup et le teardown fonctionnent de manière hiérarchique dans les describe imbriqués. Cela rappelle les zones du parc, qui ont leurs propres procédures, mais restent soumises au règlement de tout le parc :

1describe('Jurassic Park', () => {
2  beforeEach(() => console.log('Setup: Park'));
3
4  describe('Zone des prédateurs', () => {
5    beforeEach(() => console.log('Setup: Zone des prédateurs'));
6
7    it("Le T-Rex est dans l'enclos", () => {
8      console.log('Test: T-Rex');
9      // Ordre du setup : Park -> Zone des prédateurs
10    });
11  });
12
13  describe('Zone des herbivores', () => {
14    beforeEach(() => console.log('Setup: Zone des herbivores'));
15
16    it("Le Triceratops est dans l'enclos", () => {
17      console.log('Test: Triceratops');
18      // Ordre du setup : Park -> Zone des herbivores
19    });
20  });
21});

Les describe imbriqués héritent du setup des blocs parents. Le beforeEach du parent s'exécute avant le beforeEach de l'enfant. Au nettoyage, l'ordre s'inverse : d'abord le afterEach de l'enfant, puis celui du parent. Les hooks déclarés au niveau le plus haut du fichier, en dehors de tout describe, concernent chaque test de ce fichier.

Dans la leçon sur le mocking, tu utiliseras beforeEach pour nettoyer les fonctions factices, et avec le code asynchrone, pour activer les horloges factices. Dans le laboratoire ci-dessous, tu verras l'ordre des hooks affiché étape par étape.

Retiens : beforeEach est l'inspection matinale de l'enclos - chaque test commence sa journée sur un terrain propre et vérifié.

Code de cette leçon : index.js
1// Setup et Teardown - beforeEach, afterEach, beforeAll, afterAll
2console.log("=== Jurassic Park - Inspection des Enclos ===\n");
3
4// Simulation du setup/teardown
5const log = [];
6let currentDescribe = "";
7
8function logStep(step) {
9  log.push(step);
10  console.log(`  ${step}`);
11}
12
13// --- Démonstration de l'ordre ---
14console.log("--- Ordre d'exécution ---\n");
15
16// Simulation
17const steps = [];
18
19function simulateTestRun() {
20  // beforeAll
21  steps.push("1. beforeAll - connexion à la base");
22
23  // Test 1
24  steps.push("2. beforeEach - création de l'environnement");
25  steps.push("3. TEST 1 - vérification de l'enclos");
26  steps.push("4. afterEach - nettoyage");
27
28  // Test 2
29  steps.push("5. beforeEach - création de l'environnement");
30  steps.push("6. TEST 2 - vérification du capteur");
31  steps.push("7. afterEach - nettoyage");
32
33  // afterAll
34  steps.push("8. afterAll - fermeture de la base");
35}
36
37simulateTestRun();
38steps.forEach(s => console.log(s));
39
40// --- Exemple pratique ---
41console.log("\n--- Exemple pratique : beforeEach ---\n");
42
43function createEnclosure(name, capacity) {
44  return {
45    name,
46    capacity,
47    dinosaurs: [],
48    add(dino) { this.dinosaurs.push(dino); },
49    remainingCapacity() { return this.capacity - this.dinosaurs.length; }
50  };
51}
52
53// Simulation de tests avec beforeEach
54let enclosure;
55let rex;
56
57function beforeEach() {
58  enclosure = createEnclosure("Zone A", 5);
59  rex = { name: "Rex", species: "T-Rex" };
60  console.log("  [setup] Nouvel enclos et dinosaure créés");
61}
62
63// Test 1
64beforeEach();
65enclosure.add(rex);
66console.log(`  [test1] Dinosaures : ${enclosure.dinosaurs.length} -> ${enclosure.dinosaurs.length === 1 ? "PASS" : "FAIL"}`);
67
68// Test 2 - l'enclos est frais grâce à beforeEach !
69beforeEach();
70console.log(`  [test2] Enclos vide : ${enclosure.dinosaurs.length} -> ${enclosure.dinosaurs.length === 0 ? "PASS" : "FAIL"}`);
71
72// Test 3
73beforeEach();
74enclosure.add(rex);
75console.log(`  [test3] Restant : ${enclosure.remainingCapacity()} -> ${enclosure.remainingCapacity() === 4 ? "PASS" : "FAIL"}`);
76
77// --- describe imbriqués ---
78console.log("\n--- describe imbriqués ---\n");
79
80function simulateNested() {
81  const order = [];
82
83  // beforeEach externe
84  order.push("Park : beforeEach");
85
86  // Interne : Prédateurs
87  order.push("  Prédateurs : beforeEach");
88  order.push("    TEST : T-Rex dans l'enclos");
89  order.push("  Prédateurs : afterEach");
90  order.push("Park : afterEach");
91
92  order.push(""); // séparateur
93
94  order.push("Park : beforeEach");
95  order.push("  Herbivores : beforeEach");
96  order.push("    TEST : Triceratops dans l'enclos");
97  order.push("  Herbivores : afterEach");
98  order.push("Park : afterEach");
99
100  return order;
101}
102
103simulateNested().forEach(s => console.log(s));
104
105// --- beforeAll vs beforeEach ---
106console.log("\n--- beforeAll vs beforeEach ---\n");
107console.log("beforeAll :  une fois avant TOUS les tests (ex. connexion à la base)");
108console.log("beforeEach : avant CHAQUE test (ex. nettoyage des données)");
109console.log("afterEach :  après CHAQUE test (ex. réinitialisation de l'état)");
110console.log("afterAll :   une fois après TOUS les tests (ex. fermeture de la connexion)");

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. 1. Quand la fonction beforeEach s'exécute-t-elle dans Jest ?

  2. 2. Quelle est la différence entre beforeAll et beforeEach ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Écris des tests pour un système de clôtures avec beforeEach garantissant un état propre.

  • Tri vertical

    Ordonne l'ordre d'exécution des fonctions setup/teardown dans Jest :

  • Clique dans l'ordre

    Ordonne les éléments d'une déclaration beforeEach :

  • Éditeur de code

    Crée des tests avec des blocs describe imbriqués et des hooks beforeEach distincts.

Articles utiles