Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
JavaScript

Introduction aux tests unitaires

Découvrez les tests unitaires en JavaScript avec Vitest : intérêt des tests automatisés, pyramide des tests, schéma Préparer-Agir-Vérifier, installation de Vitest, écriture de tests avec describe, it ...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Découvrez les tests unitaires en JavaScript avec Vitest : intérêt des tests automatisés, pyramide des tests, schéma Préparer-Agir-Vérifier, installation de Vitest, écriture de tests avec describe, it et expect, principaux comparateurs, lecture d'un test qui échoue, valeurs limites, couverture de code et démarche TDD. Avec un exemple complet testé et des activités corrigées.
  1. Objectifs

    • Comprendre ce qu'est un test unitaire et à quoi il sert.
    • Écrire des tests selon le schéma Préparer – Agir – Vérifier (AAA).
    • Installer et utiliser Vitest: describe, it, expect et les principaux comparateurs.
    • Lire le résultat d'un test qui échoue et découvrir la démarche TDD.
  2. Pourquoi tester ?

    • Vérifier son code à la main (ouvrir la page, cliquer, regarder) est long, et on oublie toujours un cas.
    • Un test automatisé est un petit programme qui vérifie qu'une partie du code donne le résultat attendu. On le relance en une seconde après chaque modification.
    • Les tests protègent contre les régressions: une modification qui casse ce qui marchait. Ils servent aussi de documentation: ils montrent comment utiliser une fonction.
  3. Les niveaux de tests

  4. NiveauCe qu'on vérifieVitesseExemples d'outils
    UnitaireUne fonction isoléeMillisecondesVitest, Jest, node:test
    IntégrationPlusieurs modules ensembleRapideVitest, Testing Library
    De bout en bout (E2E)L'application complète dans un vrai navigateurLentePlaywright, Cypress
  • On écrit beaucoup de tests unitaires, moins de tests d'intégration et quelques tests de bout en bout : c'est la pyramide des tests.
  • Les fonctions les plus faciles à tester sont les fonctions pures: mêmes entrées, même résultat, sans toucher au DOM. D'où l'intérêt de séparer les calculs de l'affichage (leçon précédente).

Test unitaire avec Vitest : schéma Arrange Act Assert et résultat dans le terminal

  • Installer Vitest

    • Vitest est un outil de test moderne, rapide, qui comprend directement import/export. Sa syntaxe est la même que celle de Jest, très répandu.
    • Dans un projet qui a déjà un package.json avec "type": "module" (voir la leçon précédente) :
    npm install --save-dev vitest
    npm pkg set scripts.test="vitest"
    • Organisation conseillée : le code dans src/, les tests dans tests/, avec un fichier de test par module, nommé nom.test.js.
    mon-projet/
    ├── package.json
    ├── src/
    │   └── notes.js
    └── tests/
        └── notes.test.js
  • Le code à tester

    • Le module src/notes.js exporte trois fonctions pures :
    export const NOTE_MIN = 0;
    export const NOTE_MAX = 20;
    export const SEUIL_ADMISSION = 10;
    
    export function estNoteValide(note) {
      return typeof note === "number" && note >= NOTE_MIN && note <= NOTE_MAX;
    }
    
    export function calculerMoyenne(notes) {
      if (!Array.isArray(notes) || notes.length === 0) {
        throw new Error("La liste de notes est vide");
      }
      if (!notes.every(estNoteValide)) {
        throw new RangeError("Une note est hors de l'intervalle 0-20");
      }
      const total = notes.reduce((somme, note) => somme + note, 0);
      return Math.round((total / notes.length) * 100) / 100;
    }
    
    export function obtenirDecision(moyenne) {
      return moyenne >= SEUIL_ADMISSION ? "Admis" : "Ajourné";
    }
    1. Écrire les tests

    2. import { describe, it, expect } from "vitest";
      import { estNoteValide, calculerMoyenne, obtenirDecision } from "../src/notes.js";
      
      describe("calculerMoyenne", () => {
        it("calcule la moyenne de plusieurs notes", () => {
          // Arrange (préparer)
          const notes = [12, 15, 9];
          // Act (agir)
          const resultat = calculerMoyenne(notes);
          // Assert (vérifier)
          expect(resultat).toBe(12);
        });
      
        it("arrondit à deux décimales", () => {
          expect(calculerMoyenne([14, 15, 9])).toBe(12.67);
        });
      
        it("refuse une liste vide", () => {
          expect(() => calculerMoyenne([])).toThrow("vide");
        });
      
        it("refuse une note supérieure à 20", () => {
          expect(() => calculerMoyenne([12, 25])).toThrow(RangeError);
        });
      });
      
      describe("estNoteValide", () => {
        it.each([
          [0, true],
          [20, true],
          [-1, false],
          [21, false],
          ["15", false],
        ])("estNoteValide(%s) renvoie %s", (note, attendu) => {
          expect(estNoteValide(note)).toBe(attendu);
        });
      });
      
      describe("obtenirDecision", () => {
        it("admet un stagiaire qui a exactement 10", () => {
          expect(obtenirDecision(10)).toBe("Admis");
        });
      
        it("ajourne un stagiaire qui a 9.99", () => {
          expect(obtenirDecision(9.99)).toBe("Ajourné");
        });
      });
    • describe("...", () => { ... }) regroupe les tests d'une même fonction.
    • it("...", () => { ... }) (ou test) décrit un comportement attendu, rédigé comme une phrase.
    • expect(valeur) suivi d'un comparateur vérifie le résultat.
    • it.each([...]) exécute le même test avec plusieurs jeux de données.
    • Le schéma AAA: Arrange (préparer les données), Act (appeler la fonction), Assert (vérifier le résultat).
  • Les comparateurs les plus utiles

  • ComparateurVérifie que…Exemple
    toBe(x)la valeur est strictement égale (nombres, chaînes, booléens)expect(2 + 2).toBe(4)
    toEqual(x)le contenu d'un tableau ou d'un objet est identiqueexpect([1, 2]).toEqual([1, 2])
    toBeCloseTo(x)un nombre décimal est proche (évite le piège de 0.1 + 0.2)expect(0.1 + 0.2).toBeCloseTo(0.3)
    toThrow()la fonction lève une erreur (message ou type)expect(() => f()).toThrow(RangeError)
    toContain(x)un tableau ou une chaîne contient l'élémentexpect("Bootstrap").toContain("strap")
    toBeTruthy() / toBeFalsy()la valeur est « vraie » ou « fausse »expect("").toBeFalsy()
    toHaveLength(n)la longueur vaut nexpect([1, 2, 3]).toHaveLength(3)
    notinverse le comparateurexpect(5).not.toBe(3)
    Piège : expect(() => calculerMoyenne([])).toThrow() et non expect(calculerMoyenne([])).toThrow(). Sans la fonction fléchée, l'erreur se produit avant même que expect puisse l'observer.
  • Lancer les tests et lire le résultat

    • npm test lance Vitest en mode surveillance: les tests sont relancés à chaque enregistrement (touche q pour quitter). npx vitest run les exécute une seule fois.
    • Si tous les tests passent :
     ✓ tests/notes.test.js (11 tests) 4ms
    
     Test Files  1 passed (1)
          Tests  11 passed (11)
    • Imaginons qu'un collègue remplace par erreur >= par > dans obtenirDecision. Le test le détecte immédiatement :
     FAIL  tests/notes.test.js > obtenirDecision > admet un stagiaire qui a exactement 10
    AssertionError: expected 'Ajourné' to be 'Admis' // Object.is equality
    Expected: "Admis"
    Received: "Ajourné"
     ❯ tests/notes.test.js:41:33
    
     Test Files  1 failed (1)
          Tests  1 failed | 10 passed (11)
    • Le message indique le test qui échoue, la valeur attendue (Expected), la valeur obtenue (Received) et la ligne du test.
    • Couverture de code: npm install --save-dev @vitest/coverage-v8 puis npx vitest run --coverage affiche le pourcentage du code exécuté par les tests.
  • Quels cas tester ?

    • Le cas normal: une moyenne de trois notes.
    • Les valeurs limites: 0, 20, 10 tout juste, 9.99… C'est là que se cachent la plupart des bugs (> au lieu de >=).
    • Les cas d'erreur: liste vide, note négative, texte au lieu d'un nombre.
    • Un bug corrigé: on ajoute un test qui le reproduit, pour qu'il ne revienne jamais.
  • Découvrir le TDD

    • Le TDD (Test-Driven Development, développement piloté par les tests) inverse l'ordre habituel, en trois étapes répétées :
    1. Rouge: écrire un test pour une fonctionnalité qui n'existe pas encore ; il échoue.
    2. Vert: écrire le code le plus simple qui fait passer le test.
    3. Refactoriser: améliorer le code en gardant tous les tests au vert.
    • Cette démarche oblige à réfléchir au comportement attendu avant de coder.
  • Activités

  • Activité 1 : premiers tests

    • Dans le dépôt GitHub js-tests-unitaires, installez Vitest, recopiez src/notes.js et tests/notes.test.js, puis lancez npm test.
    • Modifiez volontairement une fonction pour faire échouer un test. Recopiez le message d'erreur dans le README.md et expliquez-le.

    Activité 2 : tester une fonction de validation

    • Écrivez estEmailValide(email) dans src/validation.js: elle renvoie true si la chaîne contient un seul @, au moins un caractère avant, et un point après le @.
    • Écrivez au moins 8 tests avec it.each: adresses valides, sans @, avec deux @, sans point, chaîne vide, valeur null.

    Activité 3 : TDD

    • En suivant le cycle rouge-vert-refactoriser, créez formaterNom(prenom, nom) qui renvoie "Sami BEN ALI" pour ("sami", "ben ali"): prénom avec une majuscule, nom en majuscules, espaces superflus supprimés.
    • Faites un commit à chaque étape (« rouge : … », « vert : … », « refactor : … »).

    Activité 4 : couverture

    • Installez @vitest/coverage-v8 et mesurez la couverture de votre projet. Ajoutez des tests jusqu'à atteindre 100 % pour notes.js.
    Proposition de corrigé de l'activité 2.
    // src/validation.js
    export function estEmailValide(email) {
      if (typeof email !== "string") {
        return false;
      }
      const parties = email.split("@");
      if (parties.length !== 2) {
        return false;
      }
      const [utilisateur, domaine] = parties;
      return (
        utilisateur.length > 0 &&
        domaine.includes(".") &&
        !domaine.startsWith(".") &&
        !domaine.endsWith(".")
      );
    }
    
    // tests/validation.test.js
    import { it, expect } from "vitest";
    import { estEmailValide } from "../src/validation.js";
    
    it.each([
      ["sami@exemple.tn", true],
      ["rim.benali@ecole.com", true],
      ["samiexemple.tn", false],
      ["sami@@exemple.tn", false],
      ["@exemple.tn", false],
      ["sami@exemple", false],
      ["sami@exemple.", false],
      ["", false],
      [null, false],
    ])("estEmailValide(%s) renvoie %s", (email, attendu) => {
      expect(estEmailValide(email)).toBe(attendu);
    });
  • À retenir

    • Un test unitaire vérifie automatiquement une fonction isolée ; on le relance après chaque modification.
    • Structure : describe → it → expect(...).toBe(...), selon le schéma Préparer – Agir – Vérifier.
    • On teste le cas normal, les valeurs limites et les cas d'erreur.
    • Les fonctions pures, séparées du DOM, sont les plus faciles à tester.

    Sources : MDN Web Docs (developer.mozilla.org), OWASP (owasp.org), documentations d'ESLint, Prettier et Vitest.