TDD (Test-Driven Development)
Le développement piloté par les tests, ou TDD (Test-Driven Development), est une méthodologie de développement qui inverse l’ordre traditionnel d’écriture du code. Plutôt que de d’abord écrire la logique métier puis de tester, on commence par écrire un test qui échoue, puis on écrit le code minimal nécessaire pour le faire passer, et enfin, on refactore le code si nécessaire. Ce processus est répété pour chaque nouvelle fonctionnalité.
Le cycle Red → Green → Refactor
TDD repose sur un cycle court et itératif en trois étapes fondamentales :
-
Red : écrire un test qui échoue, car la fonctionnalité n’existe pas encore.
-
Green : écrire juste assez de code pour faire passer ce test.
-
Refactor : améliorer le code tout en gardant les tests au vert.
Ce cycle est répété pour chaque nouvelle fonctionnalité, dans une approche incrémentale.
Exemple TDD avec une fonction safeDivide(a, b)
On souhaite créer une fonction qui divise a par b, en gérant les cas d’erreur, notamment :
-
la division par zéro,
-
les types non numériques,
-
le comportement avec des nombres à virgule,
-
etc.
Étape 1 : écrire le premier test (red)
import { safeDivide } from './math';
describe('safeDivide', () => {
it('should return the correct result when dividing two positive integers', () => {
expect(safeDivide(10, 2)).toBe(5);
});
});
Ce test échoue car la fonction safeDivide n’est pas encore définie.
Étape 2 : écrire le code minimal (green)
export function safeDivide(a, b) {
return a / b;
}
Le test passe. Mais on n’a pas encore traité les cas limites.
Étape 3 : ajouter un deuxième test plus complexe (red)
it('should throw an error when dividing by zero', () => {
expect(() => safeDivide(10, 0)).toThrow('Division by zero');
});
Ce test échoue, car on ne gère pas le b === 0.
Étape 4 : améliorer la logique (green)
export function safeDivide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
Les deux tests passent.
Étape 5 : couvrir d’autres cas
it('should throw if inputs are not numbers', () => {
expect(() => safeDivide('10', 2)).toThrow('Invalid input');
expect(() => safeDivide(10, null)).toThrow('Invalid input');
});
it('should support decimal numbers', () => {
expect(safeDivide(5.5, 2)).toBeCloseTo(2.75);
});
Étape 6 : implémentation plus robuste
export function safeDivide(a, b) {
if (isNaN(a) || isNaN(b)) {
throw new Error('Invalid input');
}
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
Étape 7 : Refactorisation
On pourrait, par exemple extraire les vérifications dans une fonction validateInputs(a, b)
.
Les fausses idées sur la TDD
-
❌ Écrire tous les tests d’un coup, puis le code : non. TDD est itératif.
-
❌ Écrire les tests après le code : ce n’est pas TDD, c’est du “test-after”.
-
❌ Croire que TDD garantit un bon design : les tests guident, mais ne remplacent pas la réflexion.
Pourquoi adopter la TDD ?
- Code plus fiable : Chaque régression est détectée rapidement grâce à une suite de tests construite au fil du développement.
- Design plus clair : Écrire d’abord les tests pousse à se demander comment le code sera utilisé, donc à penser à son interface avant de plonger dans l’implémentation.
- Moins de sur-implémentation : On écrit juste ce qu’il faut pour satisfaire les tests. Pas de code inutile.
Aucune page ou chapitre n'a été ajouté à cet article.