Aller au contenu

TDD — Test-Driven Development

Le cycle Red/Green/Refactor — écrire le test avant le code pour piloter la conception.


Le principe

TDD inverse l'ordre habituel : au lieu d'écrire le code puis les tests, vous ecrivez d'abord le test, puis le minimum de code pour le faire passer, puis vous refactorez.

Ce n'est pas une méthode de test — c'est une méthode de conception. Le test vous force a définir l'interface avant l'implémentation.


Le cycle Red/Green/Refactor

graph LR
    R["🔴 Red<br/>Ecrire un test<br/>qui echoue"] --> G["🟢 Green<br/>Ecrire le minimum<br/>pour que ca passe"]
    G --> RF["🔵 Refactor<br/>Nettoyer sans<br/>changer le comportement"]
    RF --> R

1. Red — écrire un test qui échoué

Ecrivez un test pour le prochain comportement que vous voulez ajouter. Le test doit échouer pour une raison claire (fonction inexistante, résultat incorrect — pas une erreur de syntaxe).

2. Green — faire passer le test

Ecrivez le minimum absolu de code pour que le test passe. Pas d'optimisation, pas de generalisation, pas de gestion d'erreurs superflue. Si le test attend 4, ecrivez return 4. Le prochain test forcera la generalisation.

3. Refactor — nettoyer

Le test passe. Maintenant, nettoyez le code et les tests sans changer le comportement. Eliminez la duplication, renommez, extrayez des fonctions. Les tests verts vous donnent un filet de sécurité.

Ne sautez pas le refactor

Le refactor est la partie la plus souvent negligee. Sans lui, le code "Green" accumule de la dette technique. Le cycle sans refactor produit du code qui marche mais qui est illisible.


Baby steps

Le TDD fonctionne par petits increments. Chaque cycle ajoute un seul comportement.

Exemple : validation d'email

Cycle 1 — une adresse valide :

# test_email.py
def test_email_valide():
    assert is_valid_email("user@example.com") is True
# email.py
def is_valid_email(email: str) -> bool:
    return True  # minimum pour passer le test

Cycle 2 — rejeter une adresse sans @ :

def test_email_sans_arobase():
    assert is_valid_email("userexample.com") is False
def is_valid_email(email: str) -> bool:
    return "@" in email  # generalisation forcee par le test

Cycle 3 — rejeter une adresse sans domaine :

def test_email_sans_domaine():
    assert is_valid_email("user@") is False
def is_valid_email(email: str) -> bool:
    if "@" not in email:
        return False
    local, domain = email.split("@", 1)
    return len(domain) > 0

Chaque test force une generalisation. Le code emerge des tests, pas d'une conception à priori.


TDD comme outil de conception

Design emergent

En TDD, vous ne concevez pas l'architecture en amont — vous la decouvrez à travers les tests. Le test vous oblige à :

  • définir l'API avant l'implémentation
  • penser aux cas d'utilisation avant les cas techniques
  • garder les unites petites et decouples (sinon les tests sont penibles a écrire)

Les signaux du test

Signal dans le test Ce que ca révélé sur le code
Test difficile à écrire L'unite a trop de responsabilités
Beaucoup de mocks nécessaires Couplage fort entre composants
Setup complexe (10+ lignes d'arrangement) L'objet a trop de dépendances
Test fragile (casse quand on refactore) Le test vérifié l'implémentation, pas le comportement

Quand pratiquer le TDD

Le TDD n'est pas toujours le bon outil. Voici un guide pragmatique :

Situation TDD recommande ?
Logique métier complexe (calculs, règles) Oui
Algorithme avec cas limites Oui
API publique / interface de librairie Oui
Code exploratoire / prototype Non
Intégration avec un service externe Non (mock ou intégration test)
Code UI / layout Non (test après)
Script one-shot Non

Le bon reflexe

Si vous hesitez, posez-vous la question : "est-ce que je sais définir le comportement attendu avant d'écrire le code ?" Si oui → TDD. Si non → explorez d'abord, testez ensuite.


Les erreurs courantes

Erreur Correction
Écrire plusieurs tests avant de coder Un seul test à la fois
Écrire plus que le minimum en Green Si le test passe, arretez. Le prochain test guidera
Sauter le Refactor C'est la moitie de la valeur du TDD
Tester des détails d'implémentation Testez le quoi, pas le comment
Vouloir 100% de couverture via TDD TDD couvre la logique métier, pas les getters/setters

Outils

Outil Langage Lien
pytest Python pytest.org
JUnit 5 Java junit.org/junit5
Jest JavaScript jestjs.io
Vitest JS/TS vitest.dev
go test Go pkg.go.dev/testing
RSpec Ruby rspec.info

Pour les exemples détaillés par langage, consultez les tutoriels :