Files
gh-samuelgarrett-claude-cod…/commands/tdd-expert.md
2025-11-30 08:53:46 +08:00

489 lines
12 KiB
Markdown

# TDD Expert
**Description**: Master test-driven development with the Red-Green-Refactor cycle for reliable, maintainable code
## Core Philosophy
Test-Driven Development (TDD) is not just about testing - it's a design methodology that leads to better code architecture, fewer bugs, and higher confidence in changes.
## The Red-Green-Refactor Cycle
### 🔴 **RED: Write a Failing Test**
Write the smallest test that would verify the next piece of functionality
- Test should fail for the right reason
- Test should be specific and focused
- Test should describe desired behavior, not implementation
### 🟢 **GREEN: Make It Pass**
Write the minimal code to make the test pass
- Don't worry about perfect code yet
- Simplest solution that works
- It's OK to hardcode initially
### 🔵 **REFACTOR: Improve the Code**
Clean up both test and production code
- Remove duplication
- Improve names
- Extract functions/classes
- Optimize if needed
- Tests must still pass
### ♻️ **REPEAT**
Continue the cycle for the next piece of functionality
## TDD Workflow
### Starting a New Feature
```
1. [ ] Write a list of test cases to implement
2. [ ] Pick the simplest test case
3. [ ] Write a failing test (RED)
4. [ ] Run the test - verify it fails
5. [ ] Write minimal code to pass (GREEN)
6. [ ] Run the test - verify it passes
7. [ ] Refactor (REFACTOR)
8. [ ] Run tests - verify still passing
9. [ ] Pick next test case
10. [ ] Repeat
```
### Test Case Brainstorming
Before coding, list out test scenarios:
```
Feature: User Login
Test Cases:
- [ ] Valid credentials → success
- [ ] Invalid password → error
- [ ] Nonexistent user → error
- [ ] Empty email → validation error
- [ ] Empty password → validation error
- [ ] SQL injection attempt → sanitized
- [ ] Rate limiting → too many attempts blocked
- [ ] Session creation on success
- [ ] Failed attempts logged
```
## Writing Good Tests
### The AAA Pattern
```javascript
test('should calculate total price with tax', () => {
// ARRANGE: Set up test data
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 1 }
];
const taxRate = 0.1;
// ACT: Execute the code under test
const result = calculateTotal(items, taxRate);
// ASSERT: Verify the result
expect(result).toBe(27.5); // (10*2 + 5*1) * 1.1
});
```
### Test Naming Conventions
```
Good test names are descriptive and follow patterns:
Pattern 1: should_ExpectedBehavior_When_StateUnderTest
✓ should_ReturnZero_When_CartIsEmpty
✓ should_ThrowError_When_PriceIsNegative
Pattern 2: When_StateUnderTest_Expect_ExpectedBehavior
✓ When_CartIsEmpty_Expect_ZeroTotal
✓ When_PriceIsNegative_Expect_Error
Pattern 3: GivenContext_WhenAction_ThenOutcome
✓ GivenEmptyCart_WhenCalculatingTotal_ThenReturnsZero
✓ GivenInvalidPrice_WhenCalculating_ThenThrowsError
```
### Test Organization
```javascript
describe('ShoppingCart', () => {
describe('addItem', () => {
it('should add item to empty cart', () => { });
it('should increment quantity for existing item', () => { });
it('should throw error for negative quantity', () => { });
});
describe('removeItem', () => {
it('should remove item from cart', () => { });
it('should do nothing if item not in cart', () => { });
});
describe('calculateTotal', () => {
it('should return 0 for empty cart', () => { });
it('should sum all item prices', () => { });
it('should apply tax rate correctly', () => { });
});
});
```
## Types of Tests
### 1. **Unit Tests**
Test individual functions/methods in isolation
```javascript
// Testing a pure function
describe('formatCurrency', () => {
it('should format USD correctly', () => {
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
});
it('should handle zero', () => {
expect(formatCurrency(0, 'USD')).toBe('$0.00');
});
it('should round to 2 decimals', () => {
expect(formatCurrency(1.234, 'USD')).toBe('$1.23');
});
});
```
### 2. **Integration Tests**
Test multiple components working together
```javascript
// Testing API endpoint with database
describe('POST /api/users', () => {
it('should create user and return ID', async () => {
const userData = { email: 'test@example.com', name: 'Test' };
const response = await request(app).post('/api/users').send(userData);
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('id');
// Verify in database
const user = await db.users.findById(response.body.id);
expect(user.email).toBe(userData.email);
});
});
```
### 3. **End-to-End Tests**
Test complete user workflows
```javascript
// Testing full user journey
describe('User Registration Flow', () => {
it('should allow new user to register and login', async () => {
// Visit registration page
await page.goto('/register');
// Fill form
await page.fill('#email', 'newuser@example.com');
await page.fill('#password', 'SecurePass123!');
await page.click('button[type="submit"]');
// Should redirect to dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('.welcome-message')).toContainText('Welcome');
});
});
```
## Test Doubles (Mocks, Stubs, Spies)
### When to Use Mocks
```javascript
// Mock external dependencies for unit tests
describe('UserService', () => {
it('should send welcome email on registration', async () => {
// Mock the email service
const mockEmailService = {
send: jest.fn().mockResolvedValue(true)
};
const userService = new UserService(mockEmailService);
await userService.registerUser({ email: 'test@example.com' });
// Verify email was sent
expect(mockEmailService.send).toHaveBeenCalledWith({
to: 'test@example.com',
template: 'welcome'
});
});
});
```
### Dependency Injection for Testability
```typescript
// Bad: Hard to test
class UserService {
async createUser(data: UserData) {
await Database.save(data); // Direct dependency
await EmailService.send(data.email); // Hard to mock
}
}
// Good: Easy to test
class UserService {
constructor(
private db: DatabaseService,
private email: EmailService
) {}
async createUser(data: UserData) {
await this.db.save(data);
await this.email.send(data.email);
}
}
// Now we can inject mocks in tests
```
## TDD Anti-Patterns to Avoid
### ❌ **Testing Implementation Details**
```javascript
// Bad: Tests implementation
test('should call helper function 3 times', () => {
const spy = jest.spyOn(obj, 'helperFunc');
obj.mainFunc();
expect(spy).toHaveBeenCalledTimes(3);
});
// Good: Tests behavior
test('should return correct result', () => {
expect(obj.mainFunc()).toBe(expectedResult);
});
```
### ❌ **Overly Complex Tests**
```javascript
// Bad: Test is harder to understand than the code
test('complex test', () => {
const data = generateComplexData();
for (let i = 0; i < data.length; i++) {
if (data[i].type === 'A') {
// ... complex logic
}
}
expect(result).toBe(/* computed value */);
});
// Good: Simple, focused test
test('should process type A items correctly', () => {
const item = { type: 'A', value: 10 };
expect(processItem(item)).toBe(20);
});
```
### ❌ **Test Interdependence**
```javascript
// Bad: Tests depend on each other
let userId;
test('create user', () => {
userId = createUser(); // Side effect
});
test('update user', () => {
updateUser(userId); // Depends on previous test
});
// Good: Each test is independent
test('create user', () => {
const userId = createUser();
expect(userId).toBeDefined();
});
test('update user', () => {
const userId = createUser(); // Setup within test
const result = updateUser(userId);
expect(result).toBe(true);
});
```
## Test Coverage Targets
### Aim for High Coverage, Not 100%
- **Statements**: 80%+ (most lines executed)
- **Branches**: 75%+ (most if/else paths tested)
- **Functions**: 90%+ (most functions tested)
- **Lines**: 80%+ (most lines covered)
### What to Prioritize
1. **Critical business logic** - Must have 100% coverage
2. **Complex algorithms** - High coverage with edge cases
3. **Public APIs** - All entry points tested
4. **Error handling** - All error paths verified
### What's OK to Skip
- Trivial getters/setters
- Framework boilerplate
- Third-party library code
- Configuration files
## TDD Best Practices
### 1. **Write Tests First** (Truly!)
```
❌ Bad: Write code first, then add tests
✓ Good: Write failing test, then make it pass
```
### 2. **One Assert Per Test** (Usually)
```javascript
// Prefer focused tests
test('should create user with correct email', () => {
const user = createUser({ email: 'test@example.com' });
expect(user.email).toBe('test@example.com');
});
test('should create user with hashed password', () => {
const user = createUser({ password: 'secret' });
expect(user.password).not.toBe('secret');
expect(user.password).toMatch(/^\$2[aby]\$/);
});
```
### 3. **Fast Tests**
```
- Unit tests: < 10ms each
- Integration tests: < 100ms each
- E2E tests: < 5s each
If slower, consider:
- Using test doubles
- Running in parallel
- Optimizing setup/teardown
```
### 4. **Reliable Tests**
```
Tests should:
- ✓ Pass consistently (no flakiness)
- ✓ Fail for the right reasons
- ✓ Be deterministic
- ✓ Not depend on external state
- ✓ Clean up after themselves
```
### 5. **Readable Tests**
```javascript
// Tests are documentation - make them clear
test('should allow user to checkout with valid cart', () => {
// Arrange: Create a realistic scenario
const cart = createCart();
cart.addItem({ id: 1, name: 'Widget', price: 9.99 });
const user = createUser({ hasPaymentMethod: true });
// Act: Perform the action
const result = checkout(cart, user);
// Assert: Verify expected outcome
expect(result.status).toBe('success');
expect(result.orderId).toBeDefined();
});
```
## TDD Workflow with Todo System
```
When implementing a feature with TDD:
Feature: Shopping Cart
[ ] Write test cases list
[ ] RED: Test for adding first item
[ ] GREEN: Implement add item (simple)
[ ] REFACTOR: Clean up code
[ ] RED: Test for adding duplicate item
[ ] GREEN: Implement quantity increment
[ ] REFACTOR: Extract logic
[ ] RED: Test for removing item
[ ] GREEN: Implement remove item
[ ] REFACTOR: Final cleanup
[ ] Verify all tests pass
[ ] Check coverage report
```
## When to Use This Skill
- When starting any new feature or function
- When fixing bugs (write test that reproduces bug first)
- When refactoring (tests ensure behavior doesn't change)
- When code quality and reliability are critical
- When you want to move fast with confidence
## Example: TDD Session
```
Task: Implement a function to validate email addresses
TEST LIST:
- [ ] Valid email with standard format
- [ ] Invalid email without @
- [ ] Invalid email without domain
- [ ] Empty string
- [ ] Null/undefined input
- [ ] Email with special characters
- [ ] Email with subdomain
---
🔴 RED - Test 1:
```javascript
test('should return true for valid email', () => {
expect(isValidEmail('user@example.com')).toBe(true);
});
// ❌ FAILS: isValidEmail is not defined
```
🟢 GREEN - Minimal implementation:
```javascript
function isValidEmail(email) {
return email === 'user@example.com'; // Hardcoded!
}
// ✅ PASSES
```
🔴 RED - Test 2:
```javascript
test('should return false for email without @', () => {
expect(isValidEmail('userexample.com')).toBe(false);
});
// ❌ FAILS: Returns true (because of hardcode)
```
🟢 GREEN - Real implementation:
```javascript
function isValidEmail(email) {
return email.includes('@');
}
// ✅ PASSES both tests
```
🔴 RED - Test 3:
```javascript
test('should return false for empty string', () => {
expect(isValidEmail('')).toBe(false);
});
// ❌ FAILS
```
🟢 GREEN - Improved:
```javascript
function isValidEmail(email) {
return email && email.includes('@') && email.includes('.');
}
// ✅ PASSES all tests
```
🔵 REFACTOR - Polish:
```javascript
function isValidEmail(email) {
if (!email) return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// ✅ PASSES all tests, cleaner implementation
```
Continue with remaining test cases...
```
---
**Remember**: TDD feels slower at first, but pays dividends in fewer bugs, better design, and faster debugging. Trust the process!