Retour au cours

frontend / javascript

Tests avec Jest

Leçon 221 exercice

Explication

Ce que vous allez apprendre

  • Structurer une suite de tests avec describe, test et expect
  • Choisir entre toBe (égalité stricte) et toEqual (égalité de structure)
  • Isoler une fonction de ses dépendances externes avec des mocks
  • Créer une fonction espionnée avec jest.fn() et vérifier ses appels
  • Remplacer un module entier avec jest.mock() pour tester sans dépendance réelle

Dans quel contexte ?

Une pull request ajoute une fonction calculerRemise(panier) avec un test expect(calculerRemise(panier)).toBe({ total: 90, remise: 10 }). Le test échoue systématiquement en CI avec un message cryptique, alors que la fonction semble correcte en la testant manuellement. Le problème : toBe compare par référence stricte (comme ===), donc deux objets au contenu identique mais à des adresses mémoire différentes ne sont jamais toBe-égaux. Remplacer toBe par toEqual, qui compare le contenu structurel, corrige immédiatement le test.

1. Pourquoi un programme qui "marche" ne suffit pas

Un programme qui "marche" au moment où on l'écrit ne garantit rien sur son comportement futur : chaque modification risque de casser silencieusement quelque chose ailleurs. Les tests automatisés forment un filet de sécurité qui se déclenche à chaque changement.

2. Le vocabulaire de base de Jest

Jest structure les tests autour de quelques mots-clés simples : describe regroupe des tests liés à une même fonctionnalité, test (ou it) définit un cas précis, et expect(...).toBe(...) formule l'attendu.

Prérequis

Aucune connaissance préalable de Jest n'est nécessaire, mais une bonne aisance avec les fonctions (leçon 2) et les Promises (leçon 11) aide à comprendre les tests de code asynchrone présentés plus loin dans cette leçon.

3. Un piège fréquent chez les débutants

toBe fait une égalité stricte, comme ===, adaptée aux valeurs primitives. toEqual fait une égalité de structure profonde, nécessaire pour comparer des objets — les confondre est une erreur très courante au début.

MatcherComparePour quel type de valeur
toBeRéférence stricte (===)Nombres, chaînes, booléens
toEqualStructure profondeObjets, tableaux
toBeCloseToValeur numérique approximativeNombres flottants

Piège fréquent

expect({ a: 1 }).toBe({ a: 1 }) échoue toujours, même si le contenu est identique : ce sont deux objets différents en mémoire. Utilisez toEqual dès que vous comparez un objet ou un tableau.

4. Isoler le code testé de ses dépendances

Un test unitaire efficace isole le code testé de ses dépendances externes — une base de données, une API réseau — grâce à des "mocks" : de fausses versions programmables de ces dépendances.

5. Les deux outils de Jest pour mocker

jest.fn() crée une fonction espionnée dont on peut vérifier les appels. jest.mock() remplace un module entier, pour tester la logique sans jamais dépendre d'un service réel, lent ou instable.

Cette leçon referme le premier grand bloc "pratique professionnelle" du cours avant d'aborder, dans les prochaines leçons, l'outillage qui transforme ce code en application livrée en production.

Commandes & code

Tests avec Jest

Écrire des suites de tests unitaires fiables en JavaScript.

js
// fichier : calculatrice.js
function additionner(a, b) {
    return a + b;
}
function diviser(a, b) {
    if (b === 0) throw new Error("Division par zero");
    return a / b;
}
module.exports = { additionner, diviser };

// fichier : api.js
async function obtenirUtilisateur(id, client) {
    const reponse = await client.get(`/utilisateurs/${id}`);
    return reponse.data;
}
module.exports = { obtenirUtilisateur };
js
// fichier : calculatrice.test.js
const { additionner, diviser } = require("./calculatrice");

// describe : regroupe des tests lies
describe("Calculatrice", () => {
    // test / it : un cas de test individuel
    test("additionne deux nombres positifs", () => {
        expect(additionner(2, 3)).toBe(5);
    });

    it("gere les nombres negatifs", () => {
        expect(additionner(-1, -1)).toBe(-2);
    });

    test("leve une erreur sur division par zero", () => {
        expect(() => diviser(10, 0)).toThrow("Division par zero");
        expect(() => diviser(10, 0)).toThrow(Error);
    });

    // beforeEach / afterEach : setup/teardown autour de CHAQUE test
    let calculatriceAvecEtat;
    beforeEach(() => {
        calculatriceAvecEtat = { historique: [] };
    });
    afterEach(() => {
        calculatriceAvecEtat = null;
    });

    // beforeAll / afterAll : setup/teardown UNE SEULE FOIS pour tout le describe
    beforeAll(() => console.log("Debut de la suite Calculatrice"));
    afterAll(() => console.log("Fin de la suite Calculatrice"));

    // test.each : parametrer plusieurs cas sans dupliquer le code
    test.each([
        [2, 3, 5],
        [-1, 1, 0],
        [0, 0, 0],
        [100, -50, 50],
    ])("additionner(%i, %i) doit valoir %i", (a, b, attendu) => {
        expect(additionner(a, b)).toBe(attendu);
    });
});

// --- Matchers courants ---
test("panorama des matchers", () => {
    expect(2 + 2).toBe(4);                          // egalite stricte (===)
    expect({ a: 1 }).toEqual({ a: 1 });                // egalite profonde de structure
    expect([1, 2, 3]).toContain(2);
    expect(0.1 + 0.2).toBeCloseTo(0.3);                  // pour les flottants
    expect(null).toBeNull();
    expect(undefined).toBeUndefined();
    expect("texte").toMatch(/ex/);
    expect(() => { throw new Error("x"); }).toThrow();
    expect([1, 2, 3]).toHaveLength(3);
});

// --- Mocks : isoler le code teste ---
test("mock d'une fonction simple", () => {
    const mockFn = jest.fn((x) => x * 2);
    expect(mockFn(5)).toBe(10);
    expect(mockFn).toHaveBeenCalledWith(5);
    expect(mockFn).toHaveBeenCalledTimes(1);
});

test("mock avec valeur de retour controlee", () => {
    const mockClient = { get: jest.fn() };
    mockClient.get.mockResolvedValue({ data: { id: 1, nom: "Alice" } });

    const { obtenirUtilisateur } = require("./api");
    return obtenirUtilisateur(1, mockClient).then((utilisateur) => {
        expect(utilisateur.nom).toBe("Alice");
        expect(mockClient.get).toHaveBeenCalledWith("/utilisateurs/1");
    });
});

// --- Mocker un module entier ---
jest.mock("./api");

// --- Tests asynchrones ---
test("test async avec async/await", async () => {
    const client = { get: jest.fn().mockResolvedValue({ data: { id: 2 } }) };
    const { obtenirUtilisateur } = require("./api");
    const resultat = await obtenirUtilisateur(2, client);
    expect(resultat.id).toBe(2);
});

// --- Spies : observer sans remplacer completement le comportement ---
test("spy sur console.error", () => {
    const spy = jest.spyOn(console, "error").mockImplementation(() => {});
    console.error("test");
    expect(spy).toHaveBeenCalledWith("test");
    spy.mockRestore();          // restaure le comportement original apres le test
});

// --- Snapshot testing : detecter des changements inattendus dans une structure ---
test("snapshot de configuration", () => {
    const config = { theme: "sombre", langue: "fr" };
    expect(config).toMatchSnapshot();
});
bash
npx jest                          # lance tous les tests
npx jest calculatrice              # filtre par nom de fichier
npx jest --coverage                 # rapport de couverture de code
npx jest --watch                     # relance automatiquement au changement de fichier

Résumé

  • describe/test structurent les suites ; beforeEach/afterEach gèrent le setup/teardown répété.
  • toBe compare par référence/valeur stricte, toEqual compare la structure en profondeur.
  • jest.fn()/jest.mock() isolent le code testé de ses dépendances externes (API, BDD, fichiers).
  • Le snapshot testing détecte les changements inattendus dans une structure de données ou un rendu.

Exercices pratiques

1 disponible
1

Mission : le test qui compare toEqual au lieu de toBe

Objectif : Choisir le bon matcher Jest selon le type de comparaison, et mocker une dépendance réseau sans jamais appeler la vraie API.

Contexte

Un développeur de Technologik écrit ce test qui échoue systématiquement, alors que la fonction testée semble correcte :

js
function creerUtilisateur(nom) {
  return { nom, actif: true };
}
test("cree un utilisateur actif", () => {
  expect(creerUtilisateur("Alice")).toBe({ nom: "Alice", actif: true }); // echoue !
});

Un second test doit appeler obtenirUtilisateur(1, client), qui fait un vrai appel réseau via client.get(...), mais l'équipe veut que la suite de tests s'exécute en quelques millisecondes, sans jamais dépendre d'un serveur réel.

Résoudre l’exercice →