Claude Code TDD: write tests first, let Claude implement, watch them pass
Test-driven development with Claude Code flips the usual workflow — you write the failing test, Claude writes the implementation, your hook runs the tests automatically. Here's the exact setup.
The TDD loop with Claude Code
You write test → Claude implements → hooks run tests → Claude fixes failures → repeat
This works because Claude Code's PostToolUse hook can run your test suite on every file write.
Step 1: Write a failing test first
Tell Claude:
I'm doing TDD. Do NOT implement anything yet. Just read this spec:
Function: calculateShipping(weight, destination)
- Under 1kg to domestic: $3.50
- Over 1kg to domestic: $3.50 + ($1.20 * kg)
- International: $12.00 flat
- Invalid weight (<=0): throw Error('Invalid weight')
Then write your test:
// shipping.test.js
const { calculateShipping } = require('./shipping');
describe('calculateShipping', () => {
test('domestic under 1kg', () => {
expect(calculateShipping(0.5, 'domestic')).toBe(3.50);
});
test('domestic over 1kg', () => {
expect(calculateShipping(2, 'domestic')).toBe(5.90);
});
test('international flat rate', () => {
expect(calculateShipping(5, 'international')).toBe(12.00);
});
test('invalid weight throws', () => {
expect(() => calculateShipping(0, 'domestic')).toThrow('Invalid weight');
});
});
Run it — it fails (no implementation yet). Good.
Step 2: Configure the PostToolUse hook
In .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "cd $CLAUDE_PROJECT_DIR && npm test -- --passWithNoTests 2>&1 | tail -20"
}
]
}
]
}
}
Now every time Claude writes a file, the test suite runs automatically. Claude sees the output.
Step 3: Ask Claude to implement
Now implement shipping.js to make all tests pass.
The test runner will fire automatically on each file write.
Don't stop until all tests pass.
Claude writes the implementation, sees the test output in the hook, fixes failures, iterates. You watch green pass line by line.
Step 4: The self-healing pattern
For complex implementations, add this to your hook command:
"command": "cd $CLAUDE_PROJECT_DIR && npm test 2>&1 | tail -30 && echo 'EXIT:' $?"
The EXIT: 0 or EXIT: 1 tells Claude whether to continue fixing or stop.
Real example: REST API TDD
// api.test.js - write this BEFORE any implementation
describe('POST /api/users', () => {
test('creates user with valid data', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', name: 'Test User' });
expect(res.status).toBe(201);
expect(res.body.id).toBeDefined();
});
test('rejects duplicate email', async () => {
// first insert
await request(app).post('/api/users').send({ email: 'dupe@example.com', name: 'First' });
// second insert same email
const res = await request(app)
.post('/api/users')
.send({ email: 'dupe@example.com', name: 'Second' });
expect(res.status).toBe(409);
});
test('validates email format', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: 'not-an-email', name: 'Test' });
expect(res.status).toBe(400);
});
});
Then:
Implement the POST /api/users endpoint. Tests will run on every write.
All 3 tests must pass before you stop.
Claude implements the route, the middleware, the validation — iterating until green.
Why this works better than asking Claude to "write tests"
When you write tests first:
- You define the contract — Claude can't guess wrong about edge cases
- Failures are specific — "expected 5.90 received 5.9" is a precision bug, not a logic bug
- Claude self-corrects — hooks show Claude exactly what's broken without you explaining
When Claude writes tests after implementation, it tests what it built — not what you wanted.
The coverage hook
Add coverage reporting to the same hook:
"command": "cd $CLAUDE_PROJECT_DIR && npm test -- --coverage --coverageThreshold='{\"global\":{\"branches\":80}}' 2>&1 | tail -30"
Now Claude won't stop until branch coverage hits 80%. It'll add edge case tests itself to meet the threshold.
Rate limits during long TDD sessions
Long TDD sessions hit Claude's rate limits hard — the loop of write → test → fix → repeat burns through tokens fast.
If you're running sessions longer than 30-40 minutes of active TDD, you'll hit the "rate limited" pause. The ANTHROPIC_BASE_URL trick routes through a proxy that gives you continuous access:
export ANTHROPIC_BASE_URL=https://simplylouie.com/api
claude # runs normally, no rate limit interruptions
$2/month at simplylouie.com — 7-day free trial, no charge today.
The full TDD setup checklist
# 1. Install test framework
npm install --save-dev jest
# 2. Add test script to package.json
# "test": "jest --watchAll=false"
# 3. Create .claude/settings.json with PostToolUse hook
# (see hook JSON above)
# 4. Write failing test
# 5. Tell Claude: "implement X, tests run automatically, stop when green"
# 6. Watch it work
The loop runs itself. Your job is writing the spec as tests, then reviewing the implementation once it's green.
Top comments (0)