VOOZH about

URL: https://dev.to/stacknotice/claude-code-tdd-force-red-green-refactor-with-hooks-claudemd-2026-26gk

⇱ Claude Code TDD: Force Red-Green-Refactor with Hooks & CLAUDE.md (2026) - DEV Community


The problem with AI-assisted TDD isn't that Claude can't write tests — it's that without constraints, Claude writes implementation first, then writes tests that match that implementation. You get 100% test coverage and zero confidence the tests catch anything.

This guide shows how to configure Claude Code so TDD isn't a guideline you might forget — it's the only workflow available.

Full guide: stacknotice.com/blog/claude-code-tdd-workflow-2026

The Fix: CLAUDE.md Rules

## Development workflow — STRICT TDD (always follow this order)

1. **RED**: Write a failing test first. Run `npm test` — confirm it FAILS.
 Do NOT write implementation before this step.

2. **GREEN**: Write the minimum code to make the test pass.
 Run `npm test` — confirm ALL tests pass.

3. **REFACTOR**: Clean up without changing behavior.
 Run tests after every change.

**Rules:**
- Never write implementation without a failing test first.
- Never write more implementation than the current test requires.
- One cycle at a time: RED → GREEN → REFACTOR before the next feature.

Hooks: Auto-Run Tests After Every Write

//.claude/settings.json{"hooks":{"PostToolUse":[{"matcher":"Write|Edit","hooks":[{"type":"command","command":"npm test -- --reporter=verbose 2>&1 | tail -20"}]}]}}

Every time Claude writes a file, your test suite runs automatically. Claude immediately sees whether the test is red or green — no manual npm test needed.

A Real TDD Cycle

Prompt:

Build a `validatePassword` function. Follow TDD per CLAUDE.md:
write the test first, confirm RED, then implement.

Requirements: 8+ chars, 1 uppercase, 1 number.
Returns { valid: boolean, errors: string[] }

Claude writes the test first:

// src/lib/password.test.ts
describe('validatePassword', () => {
 it('returns valid for a strong password', () => {
 expect(validatePassword('SecurePass1')).toEqual({ valid: true, errors: [] })
 })

 it('fails when too short', () => {
 const result = validatePassword('Sh0rt')
 expect(result.valid).toBe(false)
 expect(result.errors).toContain('Password must be at least 8 characters')
 })

 it('fails when no uppercase', () => {
 const result = validatePassword('lowercase1')
 expect(result.errors).toContain('Password must contain at least one uppercase letter')
 })

 it('fails when no number', () => {
 const result = validatePassword('NoNumbers')
 expect(result.errors).toContain('Password must contain at least one number')
 })
})

Hook fires → test output in context: RED (module not found).

Claude writes minimum implementation:

// src/lib/password.ts
export function validatePassword(password: string) {
 const errors: string[] = []

 if (password.length < 8) errors.push('Password must be at least 8 characters')
 if (!/[A-Z]/.test(password)) errors.push('Password must contain at least one uppercase letter')
 if (!/[0-9]/.test(password)) errors.push('Password must contain at least one number')

 return { valid: errors.length === 0, errors }
}

Hook fires → GREEN. All 4 tests pass.

Testing React Components

// src/components/PasswordInput.test.tsx
describe('PasswordInput', () => {
 it('renders input with label', () => {
 render(<PasswordInput label="Password" />)
 expect(screen.getByLabelText('Password')).toBeInTheDocument()
 })

 it('toggles password visibility', async () => {
 const user = userEvent.setup()
 render(<PasswordInput label="Password" />)

 const input = screen.getByLabelText('Password')
 expect(input).toHaveAttribute('type', 'password')

 await user.click(screen.getByRole('button', { name: /show password/i }))
 expect(input).toHaveAttribute('type', 'text')
 })

 it('shows strength indicator', async () => {
 const user = userEvent.setup()
 render(<PasswordInput label="Password" />)

 await user.type(screen.getByLabelText('Password'), 'StrongPass1')
 expect(screen.getByText('Strong')).toBeInTheDocument()
 })
})

Write test → confirm RED → implement → GREEN. Same cycle, same discipline.

Where TDD Proves Its Value: Regressions

Three weeks later, add a special character requirement:

Add: at least one special character. Write the failing test first.

Claude adds one test → it fails → Claude adds one line to the validator → all 5 tests pass. The existing tests are a regression net. If the new code breaks the uppercase check, you see it immediately.

Handling Claude Skipping TDD

If mid-session Claude writes implementation before tests:

Stop. You wrote implementation before the test.
Delete `src/lib/feature.ts`. Write the test first, confirm RED, then implement.

Explicit course corrections work reliably. If it keeps happening, /compact to clear context.

Pre-commit Hook

Block commits when tests fail:

# .husky/pre-commit
#!/bin/sh
npm test -- --run

Commit your .claude/settings.json alongside CLAUDE.md — every team member gets the same TDD enforcement automatically.


Full guide with Route Handler testing, component testing patterns, and multi-step examples: stacknotice.com/blog/claude-code-tdd-workflow-2026

Some comments may only be visible to logged-in visitors. Sign in to view all comments.