À 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.
-
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,expectet les principaux comparateurs. - Lire le résultat d'un test qui échoue et découvrir la démarche TDD.
-
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.
-
Les niveaux de tests
| Niveau | Ce qu'on vérifie | Vitesse | Exemples d'outils |
|---|---|---|---|
| Unitaire | Une fonction isolée | Millisecondes | Vitest, Jest, node:test |
| Intégration | Plusieurs modules ensemble | Rapide | Vitest, Testing Library |
| De bout en bout (E2E) | L'application complète dans un vrai navigateur | Lente | Playwright, 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).

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.jsonavec"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 danstests/, 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.jsexporte 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é";
}
-
Écrire les tests
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("...", () => { ... })(outest) 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
| Comparateur | Vé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 identique | expect([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ément | expect("Bootstrap").toContain("strap") |
toBeTruthy() / toBeFalsy() | la valeur est « vraie » ou « fausse » | expect("").toBeFalsy() |
toHaveLength(n) | la longueur vaut n | expect([1, 2, 3]).toHaveLength(3) |
not | inverse le comparateur | expect(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 testlance Vitest en mode surveillance: les tests sont relancés à chaque enregistrement (touche q pour quitter).npx vitest runles 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>dansobtenirDecision. 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-v8puisnpx vitest run --coverageaffiche 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 :
- Rouge: écrire un test pour une fonctionnalité qui n'existe pas encore ; il échoue.
- Vert: écrire le code le plus simple qui fait passer le test.
- 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, recopiezsrc/notes.jsettests/notes.test.js, puis lanceznpm test. - Modifiez volontairement une fonction pour faire échouer un test. Recopiez le message d'erreur dans le
README.mdet expliquez-le.
Activité 2 : tester une fonction de validation
- Écrivez
estEmailValide(email)danssrc/validation.js: elle renvoietruesi 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, valeurnull.
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-v8et mesurez la couverture de votre projet. Ajoutez des tests jusqu'à atteindre 100 % pournotes.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.