9.7 KiB
name, description
| name | description |
|---|---|
| tdd-expert | Test-Driven Development (TDD) expertise covering red-green-refactor cycle, behavior-driven development, test-first design, refactoring with confidence, TDD best practices, TDD workflow, unit testing strategies, mock-driven development, test doubles, TDD patterns, SOLID principles through testing, emergent design, incremental development, TDD anti-patterns, and production-grade TDD practices. Activates for TDD, test-driven development, red-green-refactor, test-first, behavior-driven, BDD, refactoring, test doubles, mock-driven, test design, SOLID principles, emergent design, incremental development, TDD workflow, TDD best practices, TDD patterns, Kent Beck, Robert Martin, Uncle Bob, test-first design. |
Test-Driven Development (TDD) Expert
Self-contained TDD expertise for ANY user project.
The TDD Cycle: Red-Green-Refactor
1. RED Phase: Write Failing Test
Goal: Define expected behavior through a failing test
import { describe, it, expect } from 'vitest';
import { Calculator } from './Calculator';
describe('Calculator', () => {
it('should add two numbers', () => {
const calculator = new Calculator();
expect(calculator.add(2, 3)).toBe(5); // WILL FAIL - Calculator doesn't exist
});
});
RED Checklist:
- Test describes ONE specific behavior
- Test fails for RIGHT reason (not syntax error)
- Test name is clear
- Expected behavior obvious
2. GREEN Phase: Minimal Implementation
Goal: Simplest code that makes test pass
// Calculator.ts
export class Calculator {
add(a: number, b: number): number {
return a + b; // Minimal implementation
}
}
GREEN Checklist:
- Test passes
- Code is simplest possible
- No premature optimization
- No extra features
3. REFACTOR Phase: Improve Design
Goal: Improve code quality without changing behavior
// Refactor: Support variable arguments
export class Calculator {
add(...numbers: number[]): number {
return numbers.reduce((sum, n) => sum + n, 0);
}
}
// Tests still pass!
REFACTOR Checklist:
- All tests still pass
- Code is more readable
- Removed duplication
- Better design patterns
TDD Benefits
Design Benefits:
- Forces modular, testable code
- Reveals design problems early
- Encourages SOLID principles
- Promotes simple solutions
Quality Benefits:
- 100% test coverage (by definition)
- Tests document behavior
- Regression safety net
- Faster debugging
Productivity Benefits:
- Less time debugging
- Confidence to refactor
- Faster iterations
- Clearer requirements
BDD: Behavior-Driven Development
Extension of TDD with natural language tests
Given-When-Then Pattern
describe('Shopping Cart', () => {
it('should apply 10% discount when total exceeds $100', () => {
// Given: A cart with $120 worth of items
const cart = new ShoppingCart();
cart.addItem({ price: 120, quantity: 1 });
// When: Getting the total
const total = cart.getTotal();
// Then: 10% discount applied
expect(total).toBe(108); // $120 - $12 (10%)
});
});
BDD Benefits:
- Tests readable by non-developers
- Clear business requirements
- Better stakeholder communication
- Executable specifications
TDD Patterns
Pattern 1: Test List
Before coding, list all tests needed:
Calculator Tests:
- [ ] add two positive numbers
- [ ] add negative numbers
- [ ] add zero
- [ ] add multiple numbers
- [ ] multiply two numbers
- [ ] divide two numbers
- [ ] divide by zero (error)
Work through list one by one.
Pattern 2: Fake It Till You Make It
Start with hardcoded returns, generalize later:
// Test 1: add(2, 3) = 5
add(a, b) { return 5; } // Hardcoded!
// Test 2: add(5, 7) = 12
add(a, b) { return a + b; } // Generalized
Pattern 3: Triangulation
Use multiple tests to force generalization:
// Test 1
expect(fizzbuzz(3)).toBe('Fizz');
// Test 2
expect(fizzbuzz(5)).toBe('Buzz');
// Test 3
expect(fizzbuzz(15)).toBe('FizzBuzz');
// Forces complete implementation
Pattern 4: Test Data Builders
Create test helpers for complex objects:
class UserBuilder {
private user = { name: 'Test', email: 'test@example.com', role: 'user' };
withName(name: string) {
this.user.name = name;
return this;
}
withRole(role: string) {
this.user.role = role;
return this;
}
build() {
return this.user;
}
}
// Usage
const admin = new UserBuilder().withRole('admin').build();
Refactoring with Confidence
The TDD Safety Net
Refactoring Types
1. Extract Method:
// Before
function processOrder(order) {
const total = order.items.reduce((sum, item) => sum + item.price, 0);
const tax = total * 0.1;
return total + tax;
}
// After (refactored with test safety)
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
function calculateTax(total) {
return total * 0.1;
}
function processOrder(order) {
const total = calculateTotal(order.items);
const tax = calculateTax(total);
return total + tax;
}
2. Remove Duplication:
// Tests force you to see duplication
it('should validate email', () => {
expect(validateEmail('test@example.com')).toBe(true);
expect(validateEmail('invalid')).toBe(false);
});
it('should validate phone', () => {
expect(validatePhone('+1-555-0100')).toBe(true);
expect(validatePhone('invalid')).toBe(false);
});
// Extract common validation pattern
Refactoring Workflow
1. All tests GREEN? → Continue
2. Identify code smell
3. Make small refactoring
4. Run tests → GREEN? → Continue
5. Repeat until satisfied
6. Commit
TDD Anti-Patterns
❌ Testing Implementation Details
// BAD: Testing private method
it('should call _validateEmail internally', () => {
spyOn(service, '_validateEmail');
service.createUser({ email: 'test@example.com' });
expect(service._validateEmail).toHaveBeenCalled();
});
// GOOD: Testing behavior
it('should reject invalid email', () => {
expect(() => service.createUser({ email: 'invalid' }))
.toThrow('Invalid email');
});
❌ Writing Tests After Code
// Wrong order!
1. Write implementation
2. Write tests
// Correct TDD:
1. Write test (RED)
2. Write implementation (GREEN)
3. Refactor
❌ Large Tests
// BAD: Testing multiple behaviors
it('should handle user lifecycle', () => {
const user = createUser();
updateUser(user, { name: 'New Name' });
deleteUser(user);
// Too much in one test!
});
// GOOD: One behavior per test
it('should create user', () => {
const user = createUser();
expect(user).toBeDefined();
});
it('should update user name', () => {
const user = createUser();
updateUser(user, { name: 'New Name' });
expect(user.name).toBe('New Name');
});
❌ Skipping Refactor Phase
// Don't skip refactoring!
RED → GREEN → REFACTOR → RED → GREEN → REFACTOR
↑________________↑
Always refactor!
Mock-Driven TDD
When testing with external dependencies
Strategy 1: Dependency Injection
class UserService {
constructor(private db: Database) {} // Inject dependency
async getUser(id: string) {
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
// Test with mock
const mockDb = { query: vi.fn().mockResolvedValue({ id: '123' }) };
const service = new UserService(mockDb);
Strategy 2: Interface-Based Mocking
interface EmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
class MockEmailService implements EmailService {
sent: any[] = [];
async send(to: string, subject: string, body: string) {
this.sent.push({ to, subject, body });
}
}
// Test with mock
const mockEmail = new MockEmailService();
const service = new UserService(mockEmail);
await service.registerUser({ email: 'test@example.com' });
expect(mockEmail.sent).toHaveLength(1);
SOLID Principles Through TDD
TDD naturally leads to SOLID design
Single Responsibility (SRP)
Tests reveal when class does too much:
// Many tests for one class? Split it!
describe('UserManager', () => {
// 20+ tests here → Too many responsibilities
});
// Refactor to multiple classes
describe('UserCreator', () => { /* 5 tests */ });
describe('UserValidator', () => { /* 5 tests */ });
describe('UserNotifier', () => { /* 5 tests */ });
Open/Closed (OCP)
Tests enable extension without modification:
// Testable, extensible design
interface PaymentProcessor {
process(amount: number): Promise<void>;
}
class StripeProcessor implements PaymentProcessor { }
class PayPalProcessor implements PaymentProcessor { }
Dependency Inversion (DIP)
TDD requires dependency injection:
// Testable: Depends on abstraction
class OrderService {
constructor(private payment: PaymentProcessor) {}
}
// Easy to test with mocks
const mockPayment = new MockPaymentProcessor();
const service = new OrderService(mockPayment);
Quick Reference
TDD Workflow
1. Write test (RED) → Fails ✅
2. Minimal code (GREEN) → Passes ✅
3. Refactor → Still passes ✅
4. Repeat
Test Smells
- Test too long (>20 lines)
- Multiple assertions (>3)
- Testing implementation
- Unclear test name
- Slow tests (>100ms)
- Flaky tests
When to Use TDD
✅ New features ✅ Bug fixes (add test first) ✅ Refactoring ✅ Complex logic ✅ Public APIs
❌ Throwaway prototypes ❌ UI layout (use E2E instead) ❌ Highly experimental code
This skill is self-contained and works in ANY user project.