Riadh Mnasri
← Retour au blog
9 min de lecture

Faire du vrai TDD avec Claude Code : l'étape qu'il ne faut jamais sauter

Dans l'article sur le TDD sous pression de deadline, j'explique pourquoi écrire le test avant le code fait gagner du temps. Ce que cet article ne détaille pas, c'est ce que « écrire le test avant le code » veut dire précisément : le TDD n'est pas une habitude vague, c'est un cycle avec des règles, des techniques nommées, et des raisons précises derrière chacune. Comprendre ces concepts change complètement ce qu'on peut attendre d'un assistant IA sur ce terrain, et où il faut rester vigilant.

Le cycle rouge-vert-refactor, et ce que chaque étape prouve

Le cycle popularisé par Kent Beck a trois phases, et chacune répond à une question différente :

 ROUGE                    VERT                      REFACTOR
 Le test échoue-t-il  ──▶ Le code minimal      ──▶  Peut-on améliorer
 pour la bonne raison ?    fait-il passer            le design sans
 (preuve que le test       le test, sans plus ?      changer le
 teste vraiment          quelque chose)               comportement ?
 quelque chose)

Rouge ne sert pas juste à « avoir un test qui échoue » : il prouve que le test est capable d'échouer, donc qu'il teste réellement quelque chose. Un test qui passe du premier coup, sans qu'on ait rien implémenté, est souvent un test mal écrit (une assertion toujours vraie, un mauvais objet testé), et ce défaut ne se voit que si on observe le rouge avant d'écrire le code.

Vert exige le minimum, pas la solution la plus élégante possible : l'objectif de cette phase est uniquement de satisfaire le test présent, rien d'anticipé. Résister à la tentation de généraliser tout de suite est une discipline à part entière, détaillée plus bas.

Refactor est la phase la plus souvent sautée, alors que c'est elle qui justifie tout le reste : sans elle, le TDD ne produit qu'une accumulation de code minimal jamais nettoyé. Les tests existants servent de filet de sécurité pendant cette phase : le comportement observable ne doit jamais changer, seule la structure interne du code peut bouger.

Deux façons d'atteindre le vert : implémentation évidente ou « Fake It »

Kent Beck décrit deux stratégies légitimes pour passer au vert, et le choix entre les deux dépend de la confiance qu'on a dans la solution :

StratégieQuand l'utiliserCe qu'elle donne
Implémentation évidenteLa solution est simple et sans ambiguïtéOn écrit directement le code correct
Fake ItLa solution n'est pas évidente, ou on veut avancer par petits pas prudentsOn retourne une valeur en dur qui fait passer le test, puis on généralise

Fake It n'est pas de la triche : c'est une façon délibérée de séparer deux problèmes qui, mélangés, sont plus difficiles à résoudre en même temps : « est-ce que mon test est bien formé ? » et « quelle est la bonne généralisation ? ». Le prix à payer, c'est qu'un code en dur qui reste en dur trompe quiconque le lit plus tard : Fake It n'a de sens que suivi d'une généralisation, jamais comme point d'arrêt.

La triangulation : forcer la généralisation avec un deuxième cas

Quand Fake It est utilisé, la triangulation est la technique qui force à abandonner la valeur en dur : on ajoute un deuxième test, avec des données différentes, qui ne peut pas passer avec la même valeur codée en dur. Ça oblige à écrire une vraie généralisation, pas une généralisation devinée à l'avance.

Sur MissionMatch, la décision qui rend un MatchResult éligible repose sur un score continu entre 0.0 et 1.0, comparé à un seuil. C'est un bon cas pour illustrer la séquence complète.

Test 1, et Fake It pour passer au vert au plus vite :

@Test
fun `un score de 0,5 est éligible, le seuil est inclusif`() {
    // Given un score exactement au seuil d'éligibilité
    val score = MatchingScore(0.5)

    // When on vérifie l'éligibilité
    val result = score.isAboveThreshold()

    // Then le score est éligible
    assertTrue(result)
}
// Fake It : passe le test 1, mais ne généralise rien
fun isAboveThreshold(): Boolean = true

Test 2, la triangulation : un cas que la valeur en dur ne peut pas satisfaire, ce qui force la vraie généralisation.

@Test
fun `un score de 0,3 n'est pas éligible, sous le seuil`() {
    // Given un score nettement sous le seuil d'éligibilité
    val score = MatchingScore(0.3)

    // When on vérifie l'éligibilité
    val result = score.isAboveThreshold()

    // Then le score n'est pas éligible
    assertFalse(result)
}

Ce deuxième test échoue contre l'implémentation figée (elle retourne toujours true), ce qui force la vraie généralisation :

fun isAboveThreshold(): Boolean = value >= ELIGIBILITY_THRESHOLD

Les deux tests passent maintenant, et rien dans ce code n'a été deviné à l'avance : chaque bout de généralisation a été forcé par un test précis. Vient ensuite la phase refactor : ici, le calcul tient en une ligne lisible, il n'y a pas de duplication à extraire, donc cette phase se limite à confirmer qu'aucun nettoyage supplémentaire n'est nécessaire, ce qui est un résultat de refactor tout à fait valide.

Ce qui fait qu'un test est un bon test

Le sigle F.I.R.S.T. (popularisé par Clean Code) résume les propriétés qu'un test doit avoir pour rester utile dans la durée :

PropriétéCe que ça veut direCe qui se passe si on l'ignore
FastS'exécute en millisecondes, pas en secondesPersonne ne lance la suite complète avant de commit
IndependentAucun test ne dépend de l'ordre ou du résultat d'un autreUn test cassé en cascade masque la vraie cause
RepeatableMême résultat à chaque exécution, sur toute machineDes tests « parfois rouges » qu'on finit par ignorer
Self-validatingLe résultat est un pass/fail net, pas une sortie à lireOn perd du temps à interpréter manuellement chaque échec
TimelyÉcrit juste avant le code qu'il teste, pas aprèsLe test documente le code au lieu de le spécifier

Isoler ou tester à travers de vrais objets

Deux écoles de TDD répondent différemment à « faut-il mocker les collaborateurs d'une unité testée ? ». L'école classiciste (dite « Detroit ») teste à travers de vrais objets autant que possible, et ne mocke qu'aux frontières du système (base de données, service externe). L'école mockiste (dite « London ») isole systématiquement chaque unité en mockant tous ses collaborateurs, pour que chaque test se concentre sur une seule responsabilité.

Sur un projet en architecture hexagonale comme évoqué dans l'article sur l'architecture hexagonale en pratique, l'approche classiciste s'impose naturellement à l'intérieur du domaine (les objets métier collaborent réellement entre eux dans les tests), et l'approche mockiste prend le relais à la frontière (les ports vers l'infrastructure sont mockés, puisqu'ils représentent justement la limite du domaine qu'on veut isoler).

Le piège spécifique à l'IA

Une fois ces concepts posés, ce qui se casse avec un assistant IA devient précis : demander « implémente cette fonctionnalité avec des tests » revient à sauter directement l'implémentation évidente sans jamais passer par le rouge, ni par Fake It, ni par la triangulation. Un assistant qui génère implémentation et test en même temps connaît déjà la réponse au moment d'écrire le test. Le test qui en résulte a de grandes chances de passer du premier coup, non pas parce que le comportement est correct, mais parce que le test a été écrit pour correspondre au code déjà produit.

 TDD réel                          Piège fréquent avec l'IA

 1. écrire le test    (rouge)      1. générer implémentation + test ensemble
 2. vérifier le rouge  ← preuve    2. le test passe au premier lancement
 3. écrire le code     (vert)      3. rouge jamais observé, jamais prouvé
 4. refactorer                     4. ça "a l'air" d'être du TDD

La différence ne se voit pas dans le code final : un test qui passe reste un test qui passe, qu'il ait été écrit avant ou après. Elle se voit uniquement dans le processus, et disparaît dès qu'on ne l'observe plus explicitement.

Les étapes qui préservent la discipline avec Claude Code

ÉtapeCe qu'on demande à Claude CodeCe qu'on vérifie soi-même
1. SpécifierDécrire le comportement attendu en Given/When/Then, sans mentionner l'implémentationLe cas est-il non ambigu ?
2. Test seul« Écris uniquement le test, pas l'implémentation »Le test compile-t-il et échoue-t-il pour la bonne raison ?
3. Rouge prouvéLancer le test soi-même (ou demander à Claude de le lancer) avant de continuerLe message d'échec correspond-il au comportement manquant, pas à une erreur de compilation ?
4. Vert minimal« Implémente juste ce qu'il faut pour faire passer ce test, rien de plus » (implémentation évidente ou Fake It)Le code ajouté couvre-t-il seulement le cas testé ?
5. Vert prouvéRelancer le testPasse-t-il, et les autres tests existants passent-ils toujours ?
6. Triangulation si besoinUn deuxième cas avant de généraliser une valeur en durLe nouveau test échoue-t-il contre l'implémentation figée ?
7. RefactorDemander un nettoyage, tests toujours au vert comme filetLe comportement observable a-t-il changé ? (il ne doit pas)

L'étape 3 est celle qui saute silencieusement si on ne l'exige pas explicitement : sans elle, rien ne distingue un vrai cycle rouge-vert d'un code généré d'un coup avec des tests qui l'accompagnent.

Rendre la discipline plus robuste qu'une simple demande

Demander à Claude Code de suivre ces étapes fonctionne la plupart du temps, mais reste une instruction que le modèle peut interpréter de travers sous une consigne ambiguë ou une conversation longue. Comme évoqué dans l'article sur les agents, harnais, skills et hooks, une instruction suivie par le modèle reste probabiliste, même bien formulée. Ce qui rend la discipline réellement fiable, c'est de sortir la vérification du rouge du jugement du modèle : lancer soi-même la suite de tests avant d'accepter l'implémentation, ou automatiser cette vérification dans un hook qui refuse un commit si aucun test n'a échoué puis réussi dans l'historique récent des commandes. Le TDD avec un assistant IA n'est fiable que si la preuve du rouge reste une vérification qu'on fait soi-même, jamais une affirmation qu'on accepte sur parole.