Guide TDD avec Claude Code : développement piloté par les tests avec Vitest et node:test
Pratiquez le TDD avec Claude Code grâce à des exemples Vitest, node:test, CI, hooks et prompts prêts à copier.
Claude Code peut produire du code très vite, mais la vitesse ne suffit pas. Les vrais problèmes apparaissent souvent après coup : un cas limite oublié, une régression, un calcul de remise incorrect ou une suite qui passe localement mais échoue en CI.
Le TDD, ou développement piloté par les tests, permet de canaliser cette vitesse. Le cycle est simple : écrire d’abord un test qui échoue, faire passer ce test avec l’implémentation minimale, puis refactoriser sans changer le comportement. C’est le cycle Red-Green-Refactor. Red signifie échec attendu, Green signifie comportement validé, Refactor signifie nettoyage.
Claude Code est efficace dans ce cadre parce qu’il peut proposer des cas de test, lire les erreurs, écrire une correction minimale et ajuster la CI. Le point clé est de ne pas demander simplement « implémente la fonctionnalité ». Il faut d’abord demander la preuve de l’échec. Cet article contient des exemples copiables avec Vitest et node:test, une configuration CI, des hooks Claude Code actuels et des modèles de prompts.
Pour cette mise à jour, j’ai vérifié la documentation officielle Claude Code hooks reference, Claude Code memory, Claude Code settings, Vitest Getting Started, Vitest CLI et Node.js test runner. L’exemple de hook lit le JSON reçu sur stdin et utilise tool_input.file_path, conformément à la documentation actuelle.
Le rôle de Claude Code dans le TDD
Claude Code peut écrire les tests, interpréter les sorties d’échec, produire l’implémentation minimale, ajouter la CI et résumer les risques. L’humain doit garder la main sur les règles métier, la sécurité, les contrats publics et la décision de publier.
| Étape | Tâche pour Claude Code | Revue humaine |
|---|---|---|
| Red | Écrire les tests qui échouent | A-t-il inventé des exigences ? |
| Green | Implémenter le minimum | Y a-t-il une abstraction inutile ? |
| Refactor | Nettoyer noms et duplication | Le comportement reste-t-il identique ? |
| CI | Exécuter les tests à chaque PR | La version Node est-elle réaliste ? |
| Opérations | Utiliser hooks et CLAUDE.md | L’automatisation est-elle rapide ? |
flowchart LR
A["Découper la spécification"] --> B["Red : test en échec"]
B --> C["Green : implémentation minimale"]
C --> D["Refactor : nettoyage"]
D --> E["CI et hooks relancent"]
E --> B
Exemple 1 : prix et coupons avec Vitest
Les règles de prix, les coupons et les abonnements sont de bons candidats pour le TDD, car une petite erreur peut toucher directement le revenu.
npm install -D vitest
{
"type": "module",
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {
"vitest": "^3.0.0"
}
}
Créez d’abord src/cart.test.ts.
import { describe, expect, it } from "vitest";
import { priceCart, ValidationError } from "./cart";
describe("priceCart", () => {
it("calculates subtotal and total without a coupon", () => {
const result = priceCart({
items: [
{ sku: "book", unitPriceCents: 1200, quantity: 2 },
{ sku: "video", unitPriceCents: 3000, quantity: 1 },
],
});
expect(result).toEqual({
subtotalCents: 5400,
discountCents: 0,
totalCents: 5400,
});
});
it("applies a valid percent coupon", () => {
const result = priceCart(
{
items: [{ sku: "course", unitPriceCents: 10000, quantity: 1 }],
coupon: {
code: "SPRING20",
percentOff: 20,
expiresAt: "2026-06-30T00:00:00.000Z",
},
},
{ now: new Date("2026-06-02T00:00:00.000Z") },
);
expect(result.totalCents).toBe(8000);
expect(result.discountCents).toBe(2000);
});
it("rejects expired coupons", () => {
expect(() =>
priceCart(
{
items: [{ sku: "course", unitPriceCents: 10000, quantity: 1 }],
coupon: {
code: "OLD20",
percentOff: 20,
expiresAt: "2026-05-01T00:00:00.000Z",
},
},
{ now: new Date("2026-06-02T00:00:00.000Z") },
),
).toThrow(ValidationError);
});
it("rejects zero or negative quantity", () => {
expect(() =>
priceCart({
items: [{ sku: "book", unitPriceCents: 1200, quantity: 0 }],
}),
).toThrow("quantity must be positive");
});
});
Demandez ensuite à Claude Code de confirmer Red.
Nous sommes dans l'étape Red. src/cart.test.ts existe, mais src/cart.ts n'existe pas.
À faire:
1. Exécuter npm test et confirmer l'échec.
2. Implémenter seulement le src/cart.ts minimal.
3. Ne pas ajouter d'UI, base de données, API externe ou fonctionnalité future.
4. Refactoriser seulement après le passage en Green.
Implémentation minimale pour src/cart.ts.
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
type CartItem = {
sku: string;
unitPriceCents: number;
quantity: number;
};
type Coupon = {
code: string;
percentOff: number;
expiresAt: string;
};
type CartInput = {
items: CartItem[];
coupon?: Coupon;
};
type PriceOptions = {
now?: Date;
};
export function priceCart(input: CartInput, options: PriceOptions = {}) {
if (input.items.length === 0) {
throw new ValidationError("cart must contain at least one item");
}
const subtotalCents = input.items.reduce((sum, item) => {
if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
throw new ValidationError("quantity must be positive");
}
if (!Number.isInteger(item.unitPriceCents) || item.unitPriceCents < 0) {
throw new ValidationError("unitPriceCents must be a non-negative integer");
}
return sum + item.unitPriceCents * item.quantity;
}, 0);
const discountCents = calculateDiscount(subtotalCents, input.coupon, options.now ?? new Date());
return {
subtotalCents,
discountCents,
totalCents: subtotalCents - discountCents,
};
}
function calculateDiscount(subtotalCents: number, coupon: Coupon | undefined, now: Date) {
if (!coupon) return 0;
if (coupon.percentOff <= 0 || coupon.percentOff > 100) {
throw new ValidationError("percentOff must be between 1 and 100");
}
if (new Date(coupon.expiresAt).getTime() < now.getTime()) {
throw new ValidationError("coupon expired");
}
return Math.round(subtotalCents * (coupon.percentOff / 100));
}
Exemple 2 : limites CLI avec node:test
Pour un petit outil Node, node:test évite une dépendance supplémentaire. Enregistrez ceci dans limit.test.mjs.
import test from "node:test";
import assert from "node:assert/strict";
export function parseLimit(value, fallback = 20) {
if (value === undefined || value === "") return fallback;
const parsed = Number(value);
if (!Number.isInteger(parsed)) {
throw new TypeError("limit must be an integer");
}
if (parsed < 1 || parsed > 100) {
throw new RangeError("limit must be between 1 and 100");
}
return parsed;
}
test("parseLimit uses fallback when the value is empty", () => {
assert.equal(parseLimit(undefined), 20);
assert.equal(parseLimit("", 50), 50);
});
test("parseLimit accepts values from 1 to 100", () => {
assert.equal(parseLimit("1"), 1);
assert.equal(parseLimit("100"), 100);
});
test("parseLimit rejects decimals and out-of-range values", () => {
assert.throws(() => parseLimit("1.5"), /integer/);
assert.throws(() => parseLimit("0"), /between 1 and 100/);
assert.throws(() => parseLimit("101"), /between 1 and 100/);
});
node --test limit.test.mjs
Exemple 3 : transformer un bug API en régression
Un incident de production doit devenir un test qui échoue avant la correction.
Ajoute un test de régression en TDD.
Contexte:
- POST /checkout accepte à tort les coupons expirés.
- Les coupons valides et l'achat sans coupon doivent rester fonctionnels.
Red:
- Ajouter un test qui attend 400 pour un coupon expiré.
- Confirmer que l'implémentation actuelle échoue.
Green:
- Faire la plus petite correction API.
Refactor:
- Extraire seulement la comparaison de dates dupliquée.
Retour:
- Nom du test, sortie d'échec, fichiers modifiés, commandes, risques restants.
Pour aller plus loin, consultez le guide de tests API et le guide de stratégie de tests.
CI et hooks
name: test
on:
pull_request:
push:
branches: [main]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
Hook pour lancer Vitest sur les fichiers liés après une édition.
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/run-related-vitest.mjs",
"timeout": 120
}
]
}
]
}
}
import { spawnSync } from "node:child_process";
import path from "node:path";
let raw = "";
for await (const chunk of process.stdin) {
raw += chunk;
}
const event = raw ? JSON.parse(raw) : {};
const filePath = event.tool_input?.file_path;
if (typeof filePath !== "string" || !/\.[cm]?[jt]sx?$/.test(filePath)) {
process.exit(0);
}
const target = path.isAbsolute(filePath)
? path.relative(process.cwd(), filePath)
: filePath;
const result = spawnSync("npx", ["vitest", "related", target, "--run"], {
stdio: "inherit",
shell: process.platform === "win32",
});
process.exit(result.status ?? 1);
Dans CLAUDE.md, gardez une règle courte.
## TDD workflow
- Behavior changes start with a failing test.
- Show the Red result before implementation.
- Implement the smallest change that makes the test pass.
- Refactor only after the targeted test is Green.
- Report the command, result, changed files, and remaining risk.
Voir aussi le guide hooks et les bonnes pratiques CLAUDE.md.
Modèles de prompts
Nouvelle fonctionnalité en TDD:
Objectif:
Ajouter [fonctionnalité].
Spécification:
- [cas nominal]
- [valeurs limites]
- [comportement d'erreur]
Processus:
1. Écrire les tests d'abord.
2. Exécuter npm test et montrer Red.
3. Implémenter le minimum pour Green.
4. Refactoriser sans changer le comportement.
Retour:
Sortie d'échec, commandes, fichiers modifiés, risques.
Correction de bug en TDD:
Reproduction:
[entrée, action ou log]
Attendu:
[bon comportement]
Actuel:
[comportement observé]
Demande:
Ajouter d'abord un test de régression qui échoue.
Corriger ensuite avec le plus petit changement.
Ne pas affaiblir ou supprimer les tests existants sans justification.
Refactor sécurisé:
Cible:
[fichier/fonction]
Contrainte:
Le comportement public ne change pas.
Étapes:
1. Ajouter des characterization tests.
2. Confirmer Green.
3. Modifier seulement l'intérieur.
4. Relancer les mêmes tests.
Pièges fréquents
Le premier piège est de sauter Red. Si le test passe avant la correction, il ne protège rien. Le deuxième est de tester les détails internes plutôt que le comportement. Le troisième est d’utiliser l’heure réelle dans les tests ; injectez now. Le quatrième est de trop croire aux mocks pour le paiement, l’e-mail ou le CRM. Le cinquième est de laisser Claude Code supprimer des tests pour obtenir Green.
CTA
Commencez avec une règle de prix, un parseur CLI ou une régression API. En solo, utilisez la fiche gratuite Claude Code et les modèles ci-dessus. Pour des prompts, hooks et checklists réutilisables, consultez les produits ClaudeCodeLab. Pour une équipe qui veut appliquer TDD, CI, permissions et revue sur un vrai dépôt, utilisez la formation et consultation Claude Code.
Résultat après essai
Dans le flux de Masa, demander d’abord le test en échec à Claude Code a réduit le temps de revue. Les coupons expirés, les quantités à zéro et les appels API non authentifiés sont apparus plus tôt. Exécuter toute la suite depuis un hook était trop lent ; la configuration pratique lance donc Vitest lié après les éditions et garde l’E2E complet pour la CI.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.