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 quality

4. 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 mocks

2. 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

  1. Basic Red-Green-Refactor

    claude "Teach me TDD by implementing a simple calculator"
  2. Understanding Test Doubles

    claude "Show me TDD with mocks by implementing a weather service"

Intermediate Level

  1. Complex Business Logic

    claude "Use TDD to implement a discount calculation system with multiple rules"
  2. Async Testing

    claude "Demonstrate TDD for async operations with a file upload service"

Advanced Level

  1. Architectural TDD

    claude "Use TDD to design a microservice architecture for an e-commerce system"
  2. 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

📚 External Resources


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.