test-driven-development

# Utiliser lors de l'implémentation de toute fonctionnalité ou correction de bug, avant d'écrire le code d'implémentation

npx skills add https://github.com/flutter/skills --skill test-driven-development

Test-Driven Development (TDD)

Aperçu

Écrivez le test en premier. Regardez-le échouer. Écrivez le code minimal pour passer.

Principe fondamental : Si vous n'avez pas regardé le test échouer, vous ne savez pas s'il teste la bonne chose.

Violer la lettre des règles, c'est violer l'esprit des règles.

Quand l'utiliser

Toujours :

  • Nouvelles fonctionnalités
  • Corrections de bugs
  • Refactorisation
  • Changements de comportement

Exceptions (consultez votre partenaire humain) :

  • Prototypes jetables
  • Code généré
  • Fichiers de configuration

Vous pensez « sauter TDD juste cette fois » ? Arrêtez. C'est de la rationalisation.

La Loi de fer

AUCUN CODE DE PRODUCTION SANS UN TEST ÉCHOUANT D'ABORD

Écrire du code avant le test ? Supprimez-le. Recommencez.

Sans exceptions :

  • Ne le gardez pas comme « référence »
  • Ne l'« adaptez » pas en écrivant les tests
  • Ne le regardez pas
  • Supprimer signifie supprimer

Implémentez à nouveau à partir des tests. Point.

Red-Green-Refactor

digraph tdd_cycle {
    rankdir=LR;
    red [label="RED\nÉcrire un test échouant", shape=box, style=filled, fillcolor="#ffcccc"];
    verify_red [label="Vérifier que\ncela échoue", shape=diamond];
    green [label="GREEN\nCode minimal", shape=box, style=filled, fillcolor="#ccffcc"];
    verify_green [label="Vérifier que\nc'est au vert", shape=diamond];
    refactor [label="REFACTOR\nNettoyage", shape=box, style=filled, fillcolor="#ccccff"];
    next [label="Suivant", shape=ellipse];

    red -> verify_red;
    verify_red -> green [label="oui"];
    verify_red -> red [label="mauvais\néchec"];
    green -> verify_green;
    verify_green -> refactor [label="oui"];
    verify_green -> green [label="non"];
    refactor -> verify_green [label="rester\nau vert"];
    verify_green -> next;
    next -> red;
}

RED - Écrire un test échouant

Écrivez un test minimal montrant ce qui devrait se passer.

<Good>

test('retries failed operations 3 times', async () => {
  let attempts = 0;
  const operation = () => {
    attempts++;
    if (attempts < 3) throw new Error('fail');
    return 'success';
  };

  const result = await retryOperation(operation);

  expect(result).toBe('success');
  expect(attempts).toBe(3);
});

Nom clair, teste le vrai comportement, une seule chose </Good>

<Bad>

test('retry works', async () => {
  const mock = jest.fn()
    .mockRejectedValueOnce(new Error())
    .mockRejectedValueOnce(new Error())
    .mockResolvedValueOnce('success');
  await retryOperation(mock);
  expect(mock).toHaveBeenCalledTimes(3);
});

Nom vague, teste le mock et non le code </Bad>

Exigences :

  • Un comportement
  • Nom clair
  • Code réel (pas de mocks sauf si inévitable)

Vérifier RED - Regardez-le échouer

OBLIGATOIRE. Ne sautez jamais.

npm test path/to/test.test.ts

Confirmez :

  • Le test échoue (pas d'erreurs)
  • Le message d'échec est attendu
  • Échoue parce que la fonctionnalité manque (pas de typos)

Le test passe ? Vous testez un comportement existant. Corrigez le test.

Le test génère une erreur ? Corrigez l'erreur, relancez jusqu'à ce qu'il échoue correctement.

GREEN - Code minimal

Écrivez le code le plus simple pour passer le test.

<Good>

async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
  for (let i = 0; i < 3; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === 2) throw e;
    }
  }
  throw new Error('unreachable');
}

Juste assez pour passer </Good>

<Bad>

async function retryOperation<T>(
  fn: () => Promise<T>,
  options?: {
    maxRetries?: number;
    backoff?: 'linear' | 'exponential';
    onRetry?: (attempt: number) => void;
  }
): Promise<T> {
  // YAGNI
}

Sur-ingénierie </Bad>

N'ajoutez pas de fonctionnalités, ne refactorisez pas d'autre code, et ne « améliorez » pas au-delà du test.

Vérifier GREEN - Regardez-le passer

OBLIGATOIRE.

npm test path/to/test.test.ts

Confirmez :

  • Le test passe
  • Les autres tests passent toujours
  • Résultat pristine (pas d'erreurs, d'avertissements)

Le test échoue ? Corrigez le code, pas le test.

D'autres tests échouent ? Corrigez maintenant.

REFACTOR - Nettoyage

Après le vert seulement :

  • Supprimez la duplication
  • Améliorez les noms
  • Extrayez les helpers

Gardez les tests au vert. N'ajoutez pas de comportement.

Répétez

Test échouant suivant pour la fonctionnalité suivante.

Bons tests

Qualité Bon Mauvais
Minimal Une seule chose. « et » dans le nom ? Divisez-le. test('validates email and domain and whitespace')
Clair Le nom décrit le comportement test('test1')
Montre l'intention Démontre l'API souhaitée Obscurcit ce que le code devrait faire

Pourquoi l'ordre compte

« J'écrirai les tests après pour vérifier que ça fonctionne »

Les tests écrits après le code réussissent immédiatement. Réussir immédiatement ne prouve rien :

  • Peut tester la mauvaise chose
  • Peut tester l'implémentation, pas le comportement
  • Peut manquer les cas limites que vous avez oubliés
  • Vous ne l'avez jamais vu attraper le bug

Test-first vous force à voir le test échouer, prouvant qu'il teste réellement quelque chose.

« J'ai déjà testé manuellement tous les cas limites »

Les tests manuels sont ad-hoc. Vous pensez avoir tout testé, mais :

  • Aucune trace de ce que vous avez testé
  • Impossible de relancer quand le code change
  • Facile d'oublier les cas sous pression
  • « Ça a fonctionné quand j'ai essayé » ≠ complet

Les tests automatisés sont systématiques. Ils s'exécutent de la même façon à chaque fois.

« Supprimer X heures de travail, c'est du gaspillage »

Sophisme du coût irrécupérable. Le temps est déjà parti. Votre choix maintenant :

  • Supprimez et réécrivez avec TDD (X heures de plus, haute confiance)
  • Gardez-le et ajoutez les tests après (30 minutes, faible confiance, bugs probables)

Le « gaspillage » consiste à garder du code en lequel vous ne pouvez pas avoir confiance. Du code fonctionnel sans vrais tests est de la dette technique.

« TDD est dogmatique, être pragmatique signifie s'adapter »

TDD EST pragmatique :

  • Trouve les bugs avant le commit (plus rapide que déboguer après)
  • Prévient les régressions (les tests attrapent les ruptures immédiatement)
  • Documente le comportement (les tests montrent comment utiliser le code)
  • Permet la refactorisation (modifiez librement, les tests attrapent les ruptures)

Les raccourcis « pragmatiques » = déboguer en production = plus lent.

« Les tests après atteignent les mêmes objectifs - c'est l'esprit et non le rituel »

Non. Les tests-après répondent « Qu'est-ce que cela fait ? » Les tests-first répondent « Qu'est-ce que cela devrait faire ? »

Les tests-après sont biaisés par votre implémentation. Vous testez ce que vous avez construit, pas ce qui est requis. Vous vérifiez les cas limites dont vous vous souvenez, pas ceux que vous avez découverts.

Les tests-first forcent la découverte des cas limites avant l'implémentation. Les tests-après vérifient que vous avez tout retenu (vous ne l'avez pas).

30 minutes de tests après ≠ TDD. Vous obtenez de la couverture, perdez la preuve que les tests fonctionnent.

Rationalisations courantes

Excuse Réalité
« Trop simple pour tester » Le code simple casse. Un test prend 30 secondes.
« Je vais tester après » Les tests passant immédiatement ne prouvent rien.
« Les tests après atteignent les mêmes objectifs » Tests-après = « Qu'est-ce que cela fait ? » Tests-first = « Qu'est-ce que cela devrait faire ? »
« Déjà testé manuellement » Ad-hoc ≠ systématique. Aucune trace, impossible de relancer.
« Supprimer X heures, c'est du gaspillage » Sophisme du coût irrécupérable. Garder du code non vérifié est de la dette technique.
« Garder comme référence, écrire les tests en premier » Vous l'adapterez. C'est tester après. Supprimer signifie supprimer.
« Besoin d'explorer d'abord » D'accord. Jetez l'exploration, commencez avec TDD.
« Test difficile = design peu clair » Écoutez le test. Difficile à tester = difficile à utiliser.
« TDD va me ralentir » TDD plus rapide que déboguer. Pragmatique = test-first.
« Test manuel plus rapide » Manuel ne prouve pas les cas limites. Vous testerez à nouveau à chaque changement.
« Le code existant n'a pas de tests » Vous l'améliorez. Ajoutez des tests pour le code existant.

Signaux d'alerte - ARRÊTEZ et recommencez

  • Code avant test
  • Test après implémentation
  • Le test passe immédiatement
  • Impossible d'expliquer pourquoi le test a échoué
  • Tests ajoutés « plus tard »
  • Rationaliser « juste cette fois »
  • « J'ai déjà testé manuellement »
  • « Les tests après atteignent le même objectif »
  • « C'est une question d'esprit, pas de rituel »
  • « Garder comme référence » ou « adapter du code existant »
  • « Déjà dépensé X heures, supprimer c'est du gaspillage »
  • « TDD est dogmatique, je suis pragmatique »
  • « C'est différent parce que... »

Tout cela signifie : Supprimez le code. Recommencez avec TDD.

Exemple : Correction de bug

Bug : Email vide accepté

RED

test('rejects empty email', async () => {
  const result = await submitForm({ email: '' });
  expect(result.error).toBe('Email required');
});

Vérifier RED

$ npm test
FAIL: expected 'Email required', got undefined

GREEN

function submitForm(data: FormData) {
  if (!data.email?.trim()) {
    return { error: 'Email required' };
  }
  // ...
}

Vérifier GREEN

$ npm test
PASS

REFACTOR Extrayez la validation pour plusieurs champs si nécessaire.

Liste de vérification de vérification

Avant de marquer le travail comme terminé :

  • [ ] Chaque nouvelle fonction/méthode a un test
  • [ ] Regardé chaque test échouer avant d'implémenter
  • [ ] Chaque test a échoué pour la raison attendue (fonctionnalité manquante, pas typo)
  • [ ] Écrit le code minimal pour passer chaque test
  • [ ] Tous les tests passent
  • [ ] Résultat pristine (pas d'erreurs, d'avertissements)
  • [ ] Les tests utilisent du code réel (mocks seulement si inévitable)
  • [ ] Cas limites et erreurs couverts

Impossible de cocher toutes les cases ? Vous avez sauté TDD. Recommencez.

Quand vous êtes bloqué

Problème Solution
Ne sais pas comment tester Écrivez l'API souhaitée. Écrivez d'abord l'assertion. Consultez votre partenaire humain.
Test trop compliqué Design trop compliqué. Simplifiez l'interface.
Dois mocker tout Code trop couplé. Utilisez l'injection de dépendances.
Configuration de test énorme Extrayez les helpers. Toujours complexe ? Simplifiez la conception.

Intégration du débogage

Bug trouvé ? Écrivez un test échouant le reproduisant. Suivez le cycle TDD. Le test prouve la correction et prévient la régression.

Ne corrigez jamais les bugs sans un test.

Anti-modèles de test

Lors de l'ajout de mocks ou d'utilitaires de test, lisez @testing-anti-patterns.md pour éviter les pièges courants :

  • Tester le comportement du mock au lieu du comportement réel
  • Ajouter des méthodes test-uniquement aux classes de production
  • Mocker sans comprendre les dépendances

Règle finale

Code de production → le test existe et a échoué en premier
Sinon → pas TDD

Aucune exception sans la permission de votre partenaire humain.