| name | Test-Driven Development |
| description | Write tests before code using RED-GREEN-REFACTOR cycle |
Test-Driven Development Skill
Overview
TDD transforms testing from an afterthought into the foundation of development. Tests are proof that code works correctly.
When to Invoke
- Writing new features
- Fixing bugs (Prove-It pattern)
- When asked to write tests
- Before any implementation work
Core Cycle: RED-GREEN-REFACTOR
RED Phase
Write a test that describes expected behavior. It must fail.
describe('calculateDiscount', () => {
it('should apply 10% discount for orders over $100', () => {
const result = calculateDiscount(150);
expect(result).toBe(15);
});
});
Run: npm test → Should FAIL
GREEN Phase
Write minimal code to pass the test. No extras.
function calculateDiscount(amount) {
if (amount > 100) return amount * 0.1;
return 0;
}
Run: npm test → Should PASS
REFACTOR Phase
Improve code while keeping tests green.
const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.1;
function calculateDiscount(amount) {
if (amount <= DISCOUNT_THRESHOLD) return 0;
return amount * DISCOUNT_RATE;
}
Run: npm test → Should still PASS
Prove-It Pattern (Bug Fixes)
Step 1: Write Failing Test
it('should handle empty cart without error (bug #456)', () => {
const cart = new Cart([]);
expect(() => cart.getTotal()).not.toThrow();
expect(cart.getTotal()).toBe(0);
});
Step 2: Verify Failure
Run test → Confirms bug exists
Step 3: Fix Bug
getTotal() {
if (this.items.length === 0) return 0;
return this.items.reduce((sum, item) => sum + item.price, 0);
}
Step 4: Verify Pass
Run test → Confirms fix works
Step 5: Run Full Suite
npm test
Test Pyramid
┌─────────┐
│ E2E │ 5% — Critical user flows
│ Tests │ Full system, minutes
├─────────┤
│ Integr. │ 15% — API + DB interactions
│ Tests │ Seconds
├─────────┤
│ Unit │ 80% — Pure logic
│ Tests │ Milliseconds
└─────────┘
Test Structure: AAA Pattern
it('should calculate tax for California', () => {
const order = createOrder({ state: 'CA', subtotal: 100 });
const tax = calculateTax(order);
expect(tax).toBe(7.25);
});
DAMP > DRY in Tests
Tests should be Descriptive And Meaningful Phrases.
it('should reject password without uppercase letter', () => {
const result = validatePassword('lowercase123!');
expect(result.valid).toBe(false);
expect(result.errors).toContain('Must contain uppercase letter');
});
it('should reject invalid password', () => {
expect(validate(INVALID_PASSWORD_NO_UPPER)).toBe(false);
});
Test Doubles (Preference Order)
- Real implementations — Best, but may be slow
- Fakes — In-memory DB, simplified implementations
- Stubs — Return canned responses
- Mocks — Verify interactions (use sparingly)
const db = createTestDatabase();
const user = await userService.create(db, userData);
const mockDb = { create: vi.fn().mockResolvedValue(user) };
Naming Conventions
'should return empty array when no users exist'
'should throw ValidationError when email is invalid'
'should send welcome email after registration'
'works correctly'
'test user creation'
'handles error'
Anti-Patterns
| Pattern | Problem | Solution |
|---|
| Testing internals | Breaks on refactor | Test behavior, not implementation |
| Flaky tests | Erodes trust | Use deterministic data |
| Over-mocking | False confidence | Prefer real implementations |
| Snapshot abuse | Large diffs ignored | Use sparingly |
| Shared mutable state | Tests affect each other | Reset in beforeEach |
| Testing frameworks | Wasted effort | Only test your code |
Verification Checklist