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égie | Quand l'utiliser | Ce qu'elle donne |
|---|---|---|
| Implémentation évidente | La solution est simple et sans ambiguïté | On écrit directement le code correct |
| Fake It | La solution n'est pas évidente, ou on veut avancer par petits pas prudents | On 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 dire | Ce qui se passe si on l'ignore |
|---|---|---|
| Fast | S'exécute en millisecondes, pas en secondes | Personne ne lance la suite complète avant de commit |
| Independent | Aucun test ne dépend de l'ordre ou du résultat d'un autre | Un test cassé en cascade masque la vraie cause |
| Repeatable | Même résultat à chaque exécution, sur toute machine | Des tests « parfois rouges » qu'on finit par ignorer |
| Self-validating | Le résultat est un pass/fail net, pas une sortie à lire | On perd du temps à interpréter manuellement chaque échec |
| Timely | Écrit juste avant le code qu'il teste, pas après | Le 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
| Étape | Ce qu'on demande à Claude Code | Ce qu'on vérifie soi-même |
|---|---|---|
| 1. Spécifier | Décrire le comportement attendu en Given/When/Then, sans mentionner l'implémentation | Le 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 continuer | Le 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 test | Passe-t-il, et les autres tests existants passent-ils toujours ? |
| 6. Triangulation si besoin | Un deuxième cas avant de généraliser une valeur en dur | Le nouveau test échoue-t-il contre l'implémentation figée ? |
| 7. Refactor | Demander un nettoyage, tests toujours au vert comme filet | Le 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.