Anaïs Sparesotto
Tests · QualitéIntermédiaire≈ 2h30 · 8 chapitres

Les tests automatisés

Comprendre la pyramide des tests, écrire des tests unitaires lisibles, faire du TDD, automatiser l'intégration et le end-to-end. Pour pouvoir refactorer sans angoisse.

À la fin du cours, tu sais

  • Expliquer ce qu'un test fait gagner (régression, refacto, doc vivante)
  • Écrire un test unitaire propre selon le pattern AAA
  • Pratiquer le TDD (red → green → refactor)
  • Distinguer test unitaire, d'intégration et e2e
  • Mettre en place des tests Playwright sur les parcours critiques
  • Brancher les tests en CI sans gaspiller des minutes

Prérequis

  • Maîtriser JavaScript ou TypeScript
  • Avoir un projet sur lequel pratiquer (ou en créer un minimal pour le cours)

Chapitre 1

Pourquoi tester ton code

Un test, c'est un filet de sécurité qui te laisse refactorer sans angoisse. Sans tests, chaque modification est une roulette russe.

  • Anti-régression : tu modifies une fonction, le test te dit immédiatement si tu casses l'existant
  • Refacto sereine : tu changes l'implémentation sans changer le comportement, les tests verts valident
  • Documentation vivante : un test bien nommé décrit l'intention métier mieux qu'un commentaire
  • Confiance en prod : merger un vendredi soir sans avoir la boule au ventre
  • Feedback de design : un code difficile à tester est souvent un code mal découpé

Le test comme premier·e utilisateur·ice de ton code

Écrire le test t'oblige à appeler ta fonction depuis l'extérieur. Tu réalises vite si ton API est désagréable à utiliser. C'est un signal précieux pour améliorer ton design.

Chapitre 2

La pyramide des tests

Beaucoup de tests rapides à la base, peu de tests lents au sommet. Inverser la pyramide ralentit la CI et rend les tests fragiles.

  • Unitaires (≈ 70 %) : rapides (ms), isolés, testent une fonction pure ou une classe
  • Intégration (≈ 20 %) : vérifient que plusieurs modules collaborent (DB, API interne)
  • End-to-end (≈ 10 %) : simulent un·e utilisateur·ice à travers toute la stack

L'anti-pattern du cornet de glace

Beaucoup d'équipes inversent la pyramide : peu de tests unitaires, surtout des tests e2e. Résultat : suite lente (30 min), fragile, qui décourage tout le monde de la lancer. Règle d'or : si un test unitaire suffit, n'écris pas d'intégration. Si l'intégration suffit, n'écris pas d'e2e.

Quel test pour quel cas

  • Une fonction de calcul (ex : TVA, panier) → unitaire
  • Une couche d'accès à une base ou un appel d'API interne → intégration
  • Un parcours utilisateur critique (login, paiement) → e2e

Chapitre 3

Tests unitaires : le pattern AAA

Une fonction, un comportement, un test lisible en 5 secondes. Le pattern Arrange / Act / Assert structure tes tests pour qu'ils restent compréhensibles à long terme.

AAA : Arrange, Act, Assert

tva.test.ts (Vitest)
import { describe, it, expect } from 'vitest';
import { calculerTVA } from './tva';

describe('calculerTVA', () => {
  it('applique 20% sur un montant HT', () => {
    // Arrange : préparer les données et le contexte
    const ht = 100;

    // Act : exécuter la fonction testée
    const ttc = calculerTVA(ht, 0.2);

    // Assert : vérifier le résultat
    expect(ttc).toBe(120);
  });
});

Les règles d'un bon test unitaire

  • Isolation : pas de DB, pas de réseau, pas d'horloge réelle
  • Déterministe : 100 exécutions = 100 résultats identiques
  • Rapide : on doit pouvoir lancer toute la suite en quelques secondes
  • Nom clair : it('renvoie 0 quand le panier est vide') > it('test calcul')
  • Une seule chose testée : un test = un comportement attendu

Mocks et spies pour isoler

Mock d'une dépendance externe
import { vi } from 'vitest';
import { envoyerEmail } from './email';
import { creerUtilisateur } from './users';

vi.mock('./email');

it('envoie un email de bienvenue à l\'inscription', async () => {
  await creerUtilisateur({ email: 'alice@exemple.fr' });
  expect(envoyerEmail).toHaveBeenCalledWith({
    to: 'alice@exemple.fr',
    template: 'bienvenue',
  });
});

Chapitre 4

Le TDD : Red, Green, Refactor

Écrire le test avant le code, c'est laisser le test guider le design. Le code se dessine plus proprement.

Le cycle en 3 étapes

  1. Red : tu écris un test qui échoue (la fonction n'existe pas encore, ou ne fait pas ce qu'il faut)
  2. Green : tu écris le minimum de code pour passer au vert. Pas plus.
  3. Refactor : tu nettoies le code et les tests, sans changer le comportement

Baby steps

Un comportement à la fois, pas dix cas d'un coup. Tu ajoutes un test, tu fais passer, tu nettoies. Boucle suivante. Au début ça paraît lent, à la fin du projet tu as un design propre, modulaire, testable.

Le bénéfice non-évident du TDD

Ton API est pensée depuis l'usage, pas depuis l'implémentation. Tu écris d'abord comment tu aimerais appeler la fonction, ensuite tu l'implémentes. Résultat : des API plus simples et plus intuitives.

Exemple : implémenter un panier en TDD

Étape 1 : Red
import { describe, it, expect } from 'vitest';
import { Panier } from './panier';

describe('Panier', () => {
  it('a un total de 0 quand il est vide', () => {
    const panier = new Panier();
    expect(panier.total()).toBe(0);
  });
});
Étape 2 : Green (minimum)
export class Panier {
  total() {
    return 0;
  }
}

Oui, c'est ridicule au début. Mais à l'étape 3 (refactor) puis aux étapes suivantes (ajout, suppression, remise), tu vas construire un Panier solide, fonctionnalité par fonctionnalité, toujours sous filet.

Chapitre 5

Tests d'intégration

On vérifie que plusieurs briques s'emboîtent vraiment : la DB, l'API HTTP, le file system, les services externes (mockés ou non).

Le piège à éviter

Pas de DB de prod, jamais

Utilise une DB de test dédiée. Postgres dans Docker, SQLite en mémoire, ou un schéma test_ séparé. Jamais la DB de prod, même en lecture seule.

Le pattern fixtures + rollback

Test d'intégration avec Vitest et Prisma
import { describe, it, expect, beforeEach } from 'vitest';
import { db } from './db';
import { creerUtilisateur } from './users';

beforeEach(async () => {
  await db.user.deleteMany();
});

it('persiste un utilisateur en base', async () => {
  const u = await creerUtilisateur({ email: 'alice@exemple.fr' });

  const trouve = await db.user.findUnique({ where: { id: u.id } });
  expect(trouve?.email).toBe('alice@exemple.fr');
});

Variante plus propre : encadrer chaque test par une transaction qui se termine par un ROLLBACK. Tu repars d'un état strictement identique à chaque fois, sans avoir à supprimer manuellement.

Tests d'API HTTP

import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { app } from './app';

it('POST /commandes crée une commande', async () => {
  const res = await request(app)
    .post('/commandes')
    .send({ produitIds: [1, 2] })
    .set('Authorization', 'Bearer test-token');

  expect(res.status).toBe(201);
  expect(res.body).toMatchObject({ id: expect.any(Number) });
});

Chapitre 6

End-to-end avec Playwright

Tu simules un·e vrai·e utilisateur·ice dans un vrai navigateur. Lent mais irremplaçable pour les parcours critiques.

Pourquoi Playwright en 2025

  • Auto-waiting : Playwright attend que l'élément soit prêt, pas de sleep manuel
  • Locators sémantiques : getByRole, getByLabel, résistants au refactor UI
  • Multi-navigateurs : Chrome, Firefox, Safari, en parallèle
  • Trace viewer : visualisation pas-à-pas d'un test rouge en CI
  • Isolation : chaque test = un contexte navigateur neuf

Exemple : parcours d'inscription

tests/signup.spec.ts
import { test, expect } from '@playwright/test';

test('un·e visiteur·euse peut s\'inscrire', async ({ page }) => {
  await page.goto('/signup');

  // Locators sémantiques : se basent sur l'accessibilité
  await page.getByLabel('Email').fill('test@exemple.fr');
  await page.getByLabel('Mot de passe').fill('Secret123!');
  await page.getByRole('button', { name: 'Créer mon compte' }).click();

  // Auto-waiting : on attend que l'écran de bienvenue apparaisse
  await expect(
    page.getByRole('heading', { name: 'Bienvenue' }),
  ).toBeVisible();
});

Locators sémantiques par défaut

Évite les sélecteurs CSS fragiles (.btn-primary, #submit-1234). Privilégie getByRole, getByLabel, getByText : si tu changes les classes Tailwind demain, tes tests continuent de passer. Bonus : tu valides en même temps l'accessibilité.

Ce qui vaut un test e2e

  • Login / signup
  • Paiement, processus checkout
  • Création d'une ressource critique (commande, ticket, dossier)
  • Pas : les variations de couleur, les hovers, les détails CSS (trop fragile)

Chapitre 7

Couverture de code, l'arme à double tranchant

La couverture est un signal utile, mais ce n'est pas un objectif. 100 % de couverture ne signifie pas 0 bug.

Les métriques

  • Lines : % de lignes exécutées par les tests (trompeur)
  • Branches : % de branches conditionnelles (plus utile)
  • Functions : % de fonctions appelées
  • Statements : % d'instructions
# Vitest avec couverture
npx vitest run --coverage

Le seuil raisonnable

70 à 85 % sur le code métier suffit largement. Pour une lib critique (paiement, crypto), pousse à 95+. Mais ne vise jamais 100 % juste pour le chiffre.

Le piège du 100 %

Pour atteindre 100 %, on finit par tester des getters triviaux et on rate les vrais cas limites. Mieux vaut 80 % avec de vrais tests qu'un faux 100 % qui rate ce qui compte.

Mutation testing : la vraie mesure de qualité

Outils comme Stryker : ils modifient ton code volontairement (changent un > en <, par exemple) et vérifient si tes tests le détectent. Si non, tes tests sont faibles. C'est la métrique la plus exigeante.

Chapitre 8

Tests en intégration continue

Si ce n'est pas vert en CI, ce n'est pas mergé. Les tests doivent tourner sur chaque PR, bloquer le merge en cas d'échec.

Le workflow type

.github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  unit-integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run test:unit -- --coverage
      - run: npm run test:integration

  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-traces
          path: test-results/

Stratégie selon la vitesse

  • Unit + intégration : bloquants sur chaque PR
  • E2E rapide (parcours critiques) : bloquant sur chaque PR
  • E2E lent (suite complète) : nightly ou hebdo, avec alerte Slack si rouge

Tests flaky

Un test flaky, c'est un test qui passe parfois et échoue parfois, sans changement de code. Réflexe absolu : isole-le et répare-le immédiatement. Ne l'ignore jamais avec un .skip() permanent : tu perds confiance dans toute la suite.

🛠️ Exercice optionnel

Tester un calcul de TVA en TDD

Tu vas implémenter une fonction calculerTVA(montantHT, taux) en TDD : test d'abord, code ensuite. Trois cas couvrent l'essentiel.

Ta mission

La fonction cible :

export function calculerTVA(montantHT: number, taux: number): number {
  // À implémenter
}

Trois tests à écrire (dans cet ordre) :

  1. Cas nominal : 100 € HT à 20 % de TVA renvoie 120
  2. Arrondi : 33.33 € HT à 20 % renvoie 39.996, arrondi à 40.00
  3. Erreur : un montant négatif lève une erreur "Montant négatif"

Méthode :

  1. Pour chaque test : écris-le, lance-le (il échoue, Red).
  2. Implémente le minimum dans calculerTVA pour passer au vert.
  3. Refactore si besoin, sans casser les tests précédents.
  4. Passe au test suivant.

Tu bloques ? Des indices, à dévoiler quand tu en as besoin.

Indice 1

Indice masqué.

Indice 2

Indice masqué.

Indice 3

Indice masqué.

✅ QCM de fin de cours

Teste tes acquis

10 questions, plusieurs réponses parfois possibles. Coche tout ce qui te semble juste, puis valide pour voir ton score et les explications.

  1. 1

    Quel type de test doit être le plus nombreux dans ta suite ?

  2. 2

    Que signifie le pattern AAA ?

  3. 3

    En TDD, par quoi commences-tu ?

  4. 4

    100 % de couverture garantit-il zéro bug ?

  5. 5

    Que doit-on mocker dans un test unitaire ?

  6. 6

    Quel outil e2e est devenu la référence côté JS/TS en 2025 ?

  7. 7

    Un test flaky, c'est quoi ?

  8. 8

    Quel locator Playwright est à privilégier ?

  9. 9

    Tests d'intégration sur la DB : que faire entre chaque test ?

  10. 10

    Le mutation testing sert à ?

Tu peux laisser des questions sans réponse, elles compteront comme fausses.

Tu veux ce cours pour ton équipe ?

Je peux adapter et animer ce cours pour tes formateur·ices ou tes apprenant·es, en présentiel ou en distanciel. Parlons-en pendant l'audit gratuit.

Réserver un audit gratuit →