Pourquoi le TDD n'est pas une option quand on livre vite

L'objection que j'entends le plus souvent sur le TDD, c'est qu'il ralentit quand on est pressé. En pratique, c'est l'inverse : c'est justement sous pression de deadline qu'écrire le test avant le code fait gagner du temps, pas en perdre.
Ce que la pression change vraiment
Sous pression, ce qu'on sacrifie en premier, ce n'est pas le temps de coder, c'est le temps de réfléchir à ce qu'on est en train de construire. On code plus vite, mais on code flou : le périmètre du changement se précise au fur et à mesure qu'on écrit, au lieu d'être clair avant. On découvre les cas limites en production, au pire moment possible, plutôt qu'en écrivant le test.
Écrire le test avant l'implémentation force cette clarification à se faire en premier, quand elle coûte le moins cher. Un test qu'on n'arrive pas à écrire clairement est souvent le signe qu'on n'a pas encore compris le problème, pas un problème d'outillage. C'est un signal précoce, exactement quand on en a le plus besoin.
Le vrai coût n'est pas où on croit
La critique du TDD suppose que le coût se mesure en temps d'écriture du code. Mais sur une mission freelance, le coût qui compte vraiment, c'est le temps de debug en production et le temps de compréhension du prochain développeur qui reprend le code six mois plus tard, sans le contexte qu'on avait en tête au moment d'écrire la fonctionnalité. Un test qui documente un cas limite rembourse cet investissement très vite, souvent dès le premier bug évité, et sert ensuite de documentation vivante pour quiconque touche ce code après nous.
Un exemple concret
Pour une règle qui refuse une contrepartie dont l'exposition dépasse sa limite, la convention Given/When/Then donne un test qui se lit comme une spécification, avant même d'écrire l'implémentation :
describe("validation de la limite de risque", () => {
it("refuse une exposition qui dépasse la limite de la contrepartie", () => {
// Given une contrepartie avec une limite de 1 000 000
const counterparty = aCounterparty({ riskLimit: 1_000_000 });
// When on valide une exposition de 1 200 000
const result = validateExposure(counterparty, { amount: 1_200_000 });
// Then la validation échoue avec le motif explicite
expect(result).toEqual({ valid: false, reason: "LIMIT_EXCEEDED" });
});
});
Ce test ne teste rien d'implémenté : il force à décider, avant d'écrire une seule ligne de logique, ce que "dépasser la limite" veut dire exactement (strictement supérieur ? à partir de quel seuil ?) et ce que le système doit répondre. C'est cette décision, prise tôt et explicitement, qui manque quand on code l'implémentation d'abord et qu'on ajoute des tests après coup.
Où le coût se déplace, pas où il disparaît
| Code d'abord, tests après | Test d'abord (TDD) | |
|---|---|---|
| Vitesse d'écriture initiale | Plus rapide en apparence | Légèrement plus lente au départ |
| Moment de clarification du besoin | Pendant le debug, souvent en production | Avant la première ligne de code |
| Coût d'un cas limite oublié | Bug découvert par un utilisateur ou un incident | Absence de test qui saute aux yeux en revue |
| Documentation du comportement attendu | Reconstituée après coup, si quelqu'un s'en donne la peine | Le test lui-même, à jour par construction |
Ce tableau résume l'objection la plus fréquente sous un autre angle : le TDD ne fait pas disparaître le coût de la réflexion sur les cas limites, il le déplace simplement avant l'écriture du code, quand il coûte le moins cher à corriger.
Ce que ça change concrètement dans mon flux
Sur mes projets personnels comme sur mission, la même discipline s'applique : un cas d'usage n'est pas fini tant que son test ne décrit pas explicitement son comportement attendu, y compris les cas d'erreur. J'utilise systématiquement la convention Given/When/Then, qui oblige à nommer explicitement le contexte de départ, l'action déclenchante, et le résultat attendu, avant même d'écrire une ligne d'implémentation.
Sur MissionMatch ou kotlin-counterparty-risk, chaque cas d'usage du domaine a été écrit test d'abord. Le bénéfice devient particulièrement visible sur les calculs financiers : sans un test qui fixe le comportement attendu pour un cas limite (un notionnel négatif, une date d'échéance passée, un flux de collatéral nul), il est facile de livrer un calcul qui semble correct sur les cas courants et se trompe silencieusement sur les cas rares.
Ce que je réponds à la pression
Quand une mission impose une deadline serrée, la tentation est de sauter les tests pour aller plus vite. Mon expérience est systématiquement l'inverse : c'est justement là que le TDD protège le plus, parce qu'il empêche d'accumuler une dette qu'on n'aura pas le temps de rembourser plus tard, une fois la deadline passée et l'attention retombée. Ce n'est pas du perfectionnisme : c'est la seule façon que je connaisse de garder de la vitesse sur la durée, plutôt que de l'échanger contre de la dette qu'on rembourse plus tard, avec intérêts.