Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

11.2 — Tests

🎯 Objectif : tester ce qui compte, sans transformer ta CI en marathon. La promesse — tu refactors en confiance, tu déploies sans angoisse.

À l'issue de cet axe, tu sauras :

  • Comprendre la pyramide de tests et ses anti-patterns (cône de glace)
  • Écrire des tests unitaires Vitest, Pest ou pytest
  • Mocker proprement, ou éviter les mocks quand c'est mieux
  • Tester une API avec Testcontainers ou DB en mémoire
  • Écrire des tests E2E avec Playwright sur les parcours critiques
  • Mesurer la couverture sans en faire une fin en soi

Confirmé 10 min prérequis : axes 5-10 lus

3 bénéfices concrets :

  1. Refactor sans peur — tu changes 200 lignes, les tests confirment que tu n’as rien cassé.
  2. Doc vivante — un test bien nommé est un exemple d’usage à jour.
  3. Design feedback — du code dur à tester est souvent du code mal architecturé.

Les tests ne sont pas une assurance contre tous les bugs. Ils sont un filet qui rattrape les régressions évidentes.

flowchart TD
    E2E[E2E<br/>~5%<br/>Playwright/Cypress<br/>parcours critiques]
    Int[Intégration<br/>~15-20%<br/>API + DB éphémère]
    Unit[Unitaires<br/>~75-80%<br/>Vitest, Pest, pytest]
    Unit --> Int --> E2E
Pyramide saine — beaucoup d'unitaires, moins d'intégration, peu d'E2E
UnitairesIntégrationE2E
Vitesse< 1 ms50-500 ms1-10 s
Coût d’écritureFaibleMoyenÉlevé
Coût de maintenanceFaibleMoyenTrès élevé
Couvre quoiLogique pureModules + DBParcours utilisateur
Quand échoueBug logiqueBug d’intégrationBug bout-en-bout
flowchart TD
    E2E[E2E<br/>50%<br/>SLOW]
    Int[Intégration<br/>30%]
    Unit[Unitaires<br/>20%]
    Unit --> Int --> E2E
Cône de glace — anti-pattern qui rend la CI insupportable

Beaucoup d’équipes y arrivent par dérive : “on teste partout en E2E parce que c’est plus rassurant”. Résultat :

  • CI 30 min au lieu de 3 min.
  • Tests flaky (qui échouent au hasard à cause de timeouts).
  • Personne ne lance les tests en local.
Fenêtre de terminal
npm install -D vitest @vitest/coverage-v8
tests/calculator.test.ts
import { describe, it, expect } from 'vitest';
import { add, divide } from '../src/calculator';
describe('add', () => {
it('additionne deux entiers positifs', () => {
expect(add(2, 3)).toBe(5);
});
it('additionne avec zéro', () => {
expect(add(5, 0)).toBe(5);
});
it('additionne deux négatifs', () => {
expect(add(-1, -2)).toBe(-3);
});
});
describe('divide', () => {
it('lève une erreur si on divise par zéro', () => {
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
});
});
Fenêtre de terminal
npx vitest run # une fois
npx vitest # mode watch
npx vitest --coverage # avec couverture
it('applique le discount sur le total', () => {
// Arrange
const items = [{ price: 100 }, { price: 50 }];
const code = 'SUMMER10';
// Act
const total = computeTotal(items, code);
// Assert
expect(total).toBe(135); // 150 * 0.9
});

Lisibilité maximale. Si une étape devient trop longue, c’est souvent qu’il faut extraire une helper ou simplifier l’API.

src/order.service.ts
export class OrderService {
constructor(private db: OrderRepo, private mailer: Mailer) {}
async place(order: Order) {
const saved = await this.db.save(order);
await this.mailer.send('order-placed', saved);
return saved;
}
}
// Test
import { describe, it, expect, vi } from 'vitest';
import { OrderService } from '../src/order.service';
it('appelle le mailer après avoir sauvegardé', async () => {
const fakeDb = { save: vi.fn().mockResolvedValue({ id: 1 }) };
const fakeMailer = { send: vi.fn() };
const service = new OrderService(fakeDb, fakeMailer);
await service.place({ amount: 100 });
expect(fakeDb.save).toHaveBeenCalledOnce();
expect(fakeMailer.send).toHaveBeenCalledWith('order-placed', { id: 1 });
});

vi.fn() crée un mock dont tu peux vérifier les appels.

Mock Service Worker est devenu le standard 2024+ pour tester un front qui consomme une API. Au lieu de mocker fetch à coups de vi.mock(), MSW intercepte les requêtes au niveau Service Worker (en navigateur) ou request handler (en Node) et renvoie tes réponses fixées.

Pourquoi c’est mieux que vi.mock('axios') :

ApprocheProblèmeMSW
vi.mock('axios')Tu testes ton wrapper, pas le vrai chemin réseauTu testes le code identique à la prod
vi.spyOn(global, 'fetch')Boilerplate par test, fragileHandlers déclarés une fois, réutilisables
Tests E2E avec vraie APILents, instablesMSW reste rapide (millisecondes)
tests/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'Alice',
email: 'alice@example.com',
});
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: '42', ...body }, { status: 201 });
}),
http.get('/api/error', () => HttpResponse.error()), // simule une erreur réseau
];
tests/setup.ts
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';
export const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
vitest.config.ts
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
},
});
import { http, HttpResponse } from 'msw';
import { server } from './setup';
it("affiche l'état d'erreur quand l'API retourne 500", async () => {
server.use(
http.get('/api/users/:id', () => {
return new HttpResponse(null, { status: 500 });
})
);
render(<UserCard id="42" />);
expect(await screen.findByText(/erreur/i)).toBeInTheDocument();
});

server.use() ajoute un handler qui prend le pas sur celui par défaut, le temps du test. resetHandlers() après chaque test remet à zéro.

src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
if (import.meta.env.DEV) await worker.start();

Tu lances ton front sans backend → MSW intercepte les fetch et renvoie tes mocks. Idéal pour développer une feature avant que l’API correspondante existe.

Quand tu testes un objet/string/composant qui a beaucoup de champs, écrire 30 expect(x.foo).toBe(...) est pénible. Snapshot testing capture l’output entier, le compare au prochain run.

import { expect, it } from 'vitest';
import { renderToString } from 'react-dom/server';
import { Button } from './Button';
it('Button rendu HTML stable', () => {
const html = renderToString(<Button label="Click" />);
expect(html).toMatchInlineSnapshot();
// Au 1er run, Vitest insère le snapshot dans le test :
// expect(html).toMatchInlineSnapshot(`"<button class=\\"btn\\">Click</button>"`);
// Aux runs suivants, il compare l'output actuel à ce snapshot.
});
expect(value).toMatchSnapshot(); // → fichier `.snap` séparé (gros snapshots)
expect(value).toMatchInlineSnapshot(); // → inscrit dans le test (petits snapshots)
✅ Bon usage❌ Mauvais usage
Output SQL compilé par un ORM (vérifier que tu produis la bonne query)Composant React entier (snapshot vide de sens, casse à chaque refacto CSS)
Sortie d’un parser, AST, IRRéponse API instable (timestamps, IDs)
Format de fichier généré (Markdown, YAML)Output qui dépend de la locale système

Anti-pattern : commit aveuglément un nouveau snapshot quand le test casse (« vert = OK »). Tu perds toute la valeur — relire et comprendre pourquoi le diff est apparu avant de mettre à jour.

Pour les tests visuels (composants UI), préfère Playwright toHaveScreenshot() (visual regression testing) qui prend une vraie capture pixel — plus fiable que comparer du HTML.

beforeAll(() => {
process.env.DATABASE_URL = ':memory:';
// init schéma
});
it('crée et lit un user', async () => {
await db.user.create({ email: 'a@a.com' });
const user = await db.user.findByEmail('a@a.com');
expect(user).toBeDefined();
});

Testcontainers lance un vrai PostgreSQL dans un Docker temporaire pendant les tests.

import { PostgreSqlContainer } from '@testcontainers/postgresql';
let container: StartedPostgreSqlContainer;
beforeAll(async () => {
container = await new PostgreSqlContainer().start();
process.env.DATABASE_URL = container.getConnectionUri();
await runMigrations();
}, 60_000);
afterAll(async () => {
await container.stop();
});

✅ Tu testes contre le vrai Postgres, avec ses fonctionnalités spécifiques (JSONB, full-text search, RLS). ❌ ~10 s de cold start au démarrage de la suite.

Verdict 2026 : Testcontainers pour les tests vraiment importants (queries SQL complexes), in-memory SQLite pour les tests rapides du quotidien.

Le cycle Red → Green → Refactor :

  1. Red : écris un test qui échoue (la feature n’existe pas).
  2. Green : écris le minimum de code pour passer le test.
  3. Refactor : améliore le code en gardant les tests verts.
  • Logique métier complexe avec règles précises.
  • Bug fixing — écris un test qui reproduit le bug AVANT de fixer.
  • API publique — tu écris l’usage idéal, puis l’implémentation.
  • UI très visuelle — préfère Storybook + tests E2E ciblés.
  • Code exploratoire — tu prototypes, tu écris les tests après si la feature reste.
  • Code glue / config — tester du JSON-mapping est fastidieux et low-value.

Pragmatique 2026 : TDD strict sur le domaine, tests post-écriture sur le reste.

Au lieu de tester du code, tu décris des comportements en pseudo-langage naturel :

Feature: Login
Scenario: Connexion réussie
Given je suis sur la page de login
When je saisis "alice@example.com" et "password123"
And je clique sur "Se connecter"
Then je suis redirigé vers /dashboard
And je vois "Bonjour Alice"

Outils : Cucumber (multi-langages), Behat (PHP), pytest-bdd (Python).

Verdict 2026 : utile dans des contextes métier complexes où le PO / QA écrit les scénarios. Souvent over-kill pour des MVPs et SaaS classiques où Vitest + Playwright suffisent.

Playwright (par Microsoft) est le standard 2026 pour les tests bout-en-bout.

Fenêtre de terminal
npm install -D @playwright/test
npx playwright install # télécharge les navigateurs
tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test('login → dashboard', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.click('text=Se connecter');
await page.fill('input[name="email"]', 'alice@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Bonjour Alice');
});
Fenêtre de terminal
npx playwright test
npx playwright test --ui # mode interactif
  • Garde-les peu nombreux : 5-10 tests pour les parcours critiques (login, paiement, signup).
  • Test contre une DB de test : pas de pollution, données prévisibles via fixtures.
  • Selectors stables : préfère data-testid="..." à des sélecteurs CSS qui changent au refacto.
  • Pas de await page.waitForTimeout(2000) : utilise les auto-waits Playwright (expect(...)).

Cypress reste très utilisé. Différences :

CypressPlaywright
Multi-tab
Multi-navigateurLimited (Chrome-based)Chromium, Firefox, WebKit
VitesseMoyenTrès rapide
DXExcellente UIUI moderne aussi
Mobile emulationLimitéeNative

Verdict 2026 : Playwright pour les nouveaux projets. Cypress reste valide si l’équipe est habituée.

OutilUsage
VitestStandard 2026 — rapide, ESM-first, compatible Jest
JestEncore très répandu mais moins rapide
Node.js test runnernode --test natif depuis Node 20
SupertestTests d’intégration HTTP
Testing LibraryTests composants React/Vue/Svelte
PlaywrightE2E
OutilUsage
pytestStandard absolu
pytest-asyncioTests async
httpx asyncTests d’API FastAPI
Testcontainers PythonDB éphémères
OutilUsage
PestDSL moderne, lisible
PHPUnitStandard historique
Laravel : RefreshDatabaseDB en mémoire SQLite par test
Fenêtre de terminal
npx vitest run --coverage
# génère un rapport HTML détaillé
  • Lignes exécutées au moins une fois.
  • Branches explorées (if/else, switch).
  • Fonctions appelées.
  • Si les assertions sont pertinentes (un test sans expect peut passer la ligne sans rien vérifier).
  • Les cas limites non testés.
  • La logique métier correcte.
Type de codeCouverture cible
Domaine métier80-95 % (souvent atteignable)
Couche infra (DB, HTTP)60-80 %
UI / composants50-70 % (préférer Storybook + E2E)
Glue / configinutile de viser haut

Globalement 70-80 % est sain. Au-delà, le ROI décroît vite.

Tu as 50 tests E2E Playwright et 5 tests unitaires Vitest. CI = 25 minutes. Avis ?
Ton test unitaire mock 8 dépendances pour tester une fonction. Smell ?
Tu vises 100 % de couverture. Conséquence pratique ?
  • Vitest Docsvitest.dev
  • Playwright Docsplaywright.dev
  • Pest PHPpestphp.com
  • Testing JavaScript — Kent C. Dodds (cours payant mais très bon)
  • Working Effectively with Legacy Code — Michael Feathers

Suite : 11.3 — Outils statiques — ESLint, Prettier, TypeScript strict, PHPStan.