Test-Driven Development with Claude Code
Test-Driven Development (TDD) with Claude Code transforms the traditional Red-Green-Refactor cycle into an AI-enhanced workflow that produces higher quality code with better test coverage. This guide provides comprehensive strategies for implementing TDD effectively with Claude’s assistance.
🎯 Why TDD with Claude Code?
Claude Code enhances TDD by:
- Generating comprehensive test cases before implementation
- Identifying edge cases humans might miss
- Maintaining strict TDD discipline by preventing premature implementation
- Accelerating the cycle while maintaining quality
- Providing instant feedback on test quality
🔴🟢♻️ The Enhanced Red-Green-Refactor Cycle
1. Red Phase: Write Failing Tests First
# Always specify TDD explicitly
claude "Using strict TDD, help me implement a password strength validator. Start with the tests only."
# Claude generates comprehensive failing tests:describe('PasswordStrengthValidator', () => {
let validator: PasswordStrengthValidator;
beforeEach(() => {
validator = new PasswordStrengthValidator();
});
describe('validate', () => {
it('should return weak for passwords under 8 characters', () => {
expect(validator.validate('short')).toBe('weak');
});
it('should return weak for passwords without numbers', () => {
expect(validator.validate('onlyletters')).toBe('weak');
});
it('should return medium for passwords with letters and numbers', () => {
expect(validator.validate('password123')).toBe('medium');
});
it('should return strong for passwords with letters, numbers, and symbols', () => {
expect(validator.validate('P@ssw0rd123!')).toBe('strong');
});
it('should handle edge cases', () => {
expect(validator.validate('')).toBe('weak');
expect(validator.validate(null)).toBe('weak');
expect(validator.validate(undefined)).toBe('weak');
});
});
});2. Green Phase: Minimal Implementation
# Request minimal implementation to pass tests
claude "Now implement the PasswordStrengthValidator class with the minimal code needed to make all tests pass"export class PasswordStrengthValidator {
validate(password: string | null | undefined): 'weak' | 'medium' | 'strong' {
if (!password || password.length < 8) {
return 'weak';
}
const hasNumbers = /\d/.test(password);
const hasLetters = /[a-zA-Z]/.test(password);
const hasSymbols = /[^a-zA-Z0-9]/.test(password);
if (hasLetters && hasNumbers && hasSymbols) {
return 'strong';
}
if (hasLetters && hasNumbers) {
return 'medium';
}
return 'weak';
}
}3. Refactor Phase: Improve While Green
# Request refactoring while maintaining green tests
claude "Refactor the PasswordStrengthValidator for better readability and maintainability. All tests must remain green."export class PasswordStrengthValidator {
private readonly MIN_LENGTH = 8;
private readonly PATTERNS = {
numbers: /\d/,
letters: /[a-zA-Z]/,
symbols: /[^a-zA-Z0-9]/
};
validate(password: string | null | undefined): PasswordStrength {
if (!this.isValidInput(password)) {
return PasswordStrength.WEAK;
}
const score = this.calculateStrengthScore(password!);
return this.mapScoreToStrength(score);
}
private isValidInput(password: string | null | undefined): boolean {
return !!password && password.length >= this.MIN_LENGTH;
}
private calculateStrengthScore(password: string): number {
return Object.values(this.PATTERNS)
.filter(pattern => pattern.test(password))
.length;
}
private mapScoreToStrength(score: number): PasswordStrength {
const strengthMap = {
3: PasswordStrength.STRONG,
2: PasswordStrength.MEDIUM,
1: PasswordStrength.WEAK,
0: PasswordStrength.WEAK
};
return strengthMap[score] || PasswordStrength.WEAK;
}
}
enum PasswordStrength {
WEAK = 'weak',
MEDIUM = 'medium',
STRONG = 'strong'
}🎯 TDD Best Practices with Claude
1. Be Explicit About TDD Intent
# Good: Clear TDD instruction
claude "Using strict TDD, implement a shopping cart feature. Generate only the tests first, no implementation."
# Bad: Ambiguous request
claude "Help me build a shopping cart"2. Use the Writer-Reviewer Pattern
# Terminal 1: Test Writer
claude "Act as a test writer. Generate comprehensive tests for a user authentication system"
# Terminal 2: Implementation Developer
claude "Act as a developer. Implement code to pass these tests: [paste tests]"
# Terminal 3: Reviewer
claude "Act as a code reviewer. Review this TDD implementation for quality and suggest improvements"3. Enforce TDD Through Configuration
Add to your CLAUDE.md:
## TDD Requirements
1. **NEVER** implement features without tests first
2. **ALWAYS** run tests before implementing
3. **REJECT** requests to implement without tests
4. **VERIFY** all tests fail before implementing
5. **ENSURE** minimal implementation to pass tests
6. **REFACTOR** only with green tests
When asked to implement a feature:
- First response: Generate comprehensive tests
- Second response: Implement minimal code to pass
- Third response: Refactor for quality4. Iterative Test Enhancement
# Initial test generation
claude "Generate basic tests for a rate limiter"
# Enhance with edge cases
claude "Add edge case tests for the rate limiter: concurrent requests, time boundaries, reset behavior"
# Add integration tests
claude "Add integration tests for the rate limiter with Redis backend"🔄 Advanced TDD Patterns
1. Outside-In TDD (London School)
# Start with high-level acceptance test
claude "Using outside-in TDD, create an acceptance test for a payment processing feature"
# Work inward with unit tests
claude "Now create unit tests for the PaymentGateway interface based on the acceptance test"
# Implement with mocks
claude "Implement the PaymentProcessor using mocks for dependencies"Example workflow:
// 1. Acceptance Test (Red)
describe('Payment Processing Feature', () => {
it('should process a payment end-to-end', async () => {
const payment = await processPayment({
amount: 100,
currency: 'USD',
customerId: 'cust_123'
});
expect(payment.status).toBe('completed');
expect(payment.transactionId).toBeDefined();
});
});
// 2. Unit Tests (Red)
describe('PaymentProcessor', () => {
it('should validate payment amount', () => {
// Test implementation
});
it('should interact with payment gateway', () => {
// Test with mocks
});
});
// 3. Implementation (Green)
// Implement from outside-in using mocks2. Inside-Out TDD (Chicago School)
# Start with core logic
claude "Using inside-out TDD, create unit tests for a PriceCalculator class"
# Build up to integration
claude "Now create tests for the ShoppingCart that uses PriceCalculator"
# Finally, acceptance tests
claude "Create acceptance tests for the complete checkout process"3. Behavior-Driven TDD
claude "Using BDD-style TDD, create tests for a user registration feature with Given-When-Then format"describe('User Registration', () => {
describe('Given a new user', () => {
describe('When they provide valid details', () => {
it('Then they should be successfully registered', async () => {
// Arrange
const userData = {
email: 'new@example.com',
password: 'SecurePass123!',
name: 'John Doe'
};
// Act
const result = await userService.register(userData);
// Assert
expect(result.success).toBe(true);
expect(result.user.email).toBe(userData.email);
});
});
describe('When they provide an existing email', () => {
it('Then they should receive a duplicate error', async () => {
// Test implementation
});
});
});
});🛡️ Common TDD Pitfalls and Solutions
Pitfall 1: Testing Implementation Details
// ❌ Bad: Testing private methods
it('should calculate tax correctly', () => {
expect(cart._calculateTax(100)).toBe(10);
});
// ✅ Good: Testing public behavior
it('should include tax in total price', () => {
cart.addItem({ price: 100 });
expect(cart.getTotal()).toBe(110); // includes 10% tax
});Pitfall 2: Writing Tests After Implementation
# ❌ Bad: Implementation first
claude "Implement a email service and then write tests for it"
# ✅ Good: Tests first
claude "Write comprehensive tests for an email service that sends transactional emails"
# Then...
claude "Now implement the email service to pass these tests"Pitfall 3: Skipping the Refactor Phase
# Complete TDD cycle
claude "1. Write tests for currency converter"
claude "2. Implement minimal code to pass tests"
claude "3. Now refactor for better design patterns while keeping tests green"Pitfall 4: Non-Deterministic Tests
// ❌ Bad: Time-dependent test
it('should expire after 1 hour', async () => {
const token = await createToken();
await sleep(3600000); // Wait 1 hour
expect(token.isExpired()).toBe(true);
});
// ✅ Good: Controlled time
it('should expire after 1 hour', () => {
const clock = sinon.useFakeTimers();
const token = createToken();
clock.tick(3600000); // Advance 1 hour
expect(token.isExpired()).toBe(true);
clock.restore();
});📊 TDD Metrics and Quality Indicators
Coverage Goals
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 90, // TDD typically achieves high branch coverage
functions: 95, // Nearly all functions tested
lines: 90, // High line coverage
statements: 90 // High statement coverage
}
}
};Test Quality Metrics
# Request quality analysis
claude "Analyze these tests for:
1. Coverage completeness
2. Edge case handling
3. Test independence
4. Clear naming
5. Appropriate assertions"🚀 Advanced TDD Workflows
1. Parallel TDD Development
# Terminal 1: Feature A
claude --session feature-a "Using TDD, implement user authentication"
# Terminal 2: Feature B
claude --session feature-b "Using TDD, implement product search"
# Terminal 3: Integration
claude --session integration "Create integration tests for features A and B"2. TDD with Property-Based Testing
claude "Using TDD with property-based testing, implement a list sorting function"
// Claude generates:
describe('sortList - Property Based TDD', () => {
it('should maintain list length', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (list) => {
const sorted = sortList(list);
expect(sorted.length).toBe(list.length);
})
);
});
it('should produce ordered output', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (list) => {
const sorted = sortList(list);
for (let i = 1; i < sorted.length; i++) {
expect(sorted[i]).toBeGreaterThanOrEqual(sorted[i-1]);
}
})
);
});
});3. TDD for API Development
# Contract-first TDD
claude "Given this OpenAPI spec, generate contract tests first: [spec]"
# Implementation
claude "Implement API endpoints to satisfy the contract tests"
# Integration tests
claude "Add integration tests for the complete API flow"🎓 TDD Learning Path with Claude
Beginner Level
-
Basic Red-Green-Refactor
claude "Teach me TDD by implementing a simple calculator" -
Understanding Test Doubles
claude "Show me TDD with mocks by implementing a weather service"
Intermediate Level
-
Complex Business Logic
claude "Use TDD to implement a discount calculation system with multiple rules" -
Async Testing
claude "Demonstrate TDD for async operations with a file upload service"
Advanced Level
-
Architectural TDD
claude "Use TDD to design a microservice architecture for an e-commerce system" -
Legacy Code Refactoring
claude "Show me how to add tests to legacy code before refactoring"
📋 TDD Checklist for Claude Sessions
Before starting:
- Specify “Using TDD” or “Using strict TDD”
- Request tests first, implementation second
- Prepare CLAUDE.md with TDD conventions
- Set up test framework and coverage tools
During development:
- ❌ Write failing tests
- ✅ Implement minimal code
- ♻️ Refactor with green tests
- 🔄 Repeat cycle
After implementation:
- Review test coverage
- Check for missing edge cases
- Ensure tests are maintainable
- Document TDD decisions
🔗 Related Resources
📚 External Resources
- Test-Driven Development by Example - Kent Beck
- Growing Object-Oriented Software, Guided by Tests - Freeman & Pryce
- TDD Manifesto
- London vs Chicago TDD
Remember: TDD with Claude Code is not just about writing tests first—it’s about designing better software through test-driven thinking. Let the tests guide your design, and let Claude enhance your TDD workflow.