--- name: tdd-workflow description: Test-Driven Development workflow with RED-GREEN-REFACTOR cycle, coverage verification, and quality gates. Use when planning or implementing features with TDD. --- # TDD Workflow Skill ## The TDD Cycle ``` ┌─────────────────────────────────────────────────────┐ │ │ │ ┌─────┐ ┌───────┐ ┌──────────┐ │ │ │ RED │ ──▶ │ GREEN │ ──▶ │ REFACTOR │ ──┐ │ │ └─────┘ └───────┘ └──────────┘ │ │ │ ▲ │ │ │ └──────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ ``` ## Phase 1: RED (Write Failing Test) ### Rules - Write ONE test that describes desired behavior - Test must fail for the RIGHT reason - NO production code exists yet ### Checklist - [ ] Test describes behavior, not implementation - [ ] Test name clearly states expected outcome - [ ] Test uses factory functions for data - [ ] Test fails with meaningful error message - [ ] Error indicates missing behavior, not syntax/import errors ### Example ```typescript // STEP 1: Write the failing test describe('PaymentProcessor', () => { it('should reject payments with negative amounts', () => { const payment = getMockPayment({ amount: -100 }); const result = processPayment(payment); expect(result.success).toBe(false); expect(result.error).toBe('Amount must be positive'); }); }); // At this point: // - processPayment doesn't exist // - Running test gives: "ReferenceError: processPayment is not defined" // - This is the RIGHT failure - the function doesn't exist yet ``` ## Phase 2: GREEN (Write Minimum Code) ### Rules - Write ONLY enough code to pass the test - No additional functionality - No "while you're there" improvements - Ugly code is acceptable (we'll refactor) ### Checklist - [ ] Implementation makes test pass - [ ] No code beyond what test demands - [ ] All existing tests still pass - [ ] Coverage increased for new code ### Example ```typescript // STEP 2: Write minimum code to pass export function processPayment(payment: Payment): PaymentResult { if (payment.amount < 0) { return { success: false, error: 'Amount must be positive' }; } return { success: true }; } // This passes the test - that's enough for now // Don't add validation for zero, max amounts, etc. // Those need their own tests first! ``` ## Phase 3: REFACTOR (Improve Code Quality) ### Rules - COMMIT before refactoring - Only refactor if it adds value - All tests must stay green - No new functionality ### When to Refactor - Duplicate knowledge (not just similar code) - Unclear names - Complex conditionals - Magic numbers/strings ### When NOT to Refactor - Code is already clean - Similarity is structural, not semantic - Would add unnecessary abstraction ### Example ```typescript // STEP 3: Assess and refactor // BEFORE REFACTORING: git commit -m "feat: add negative amount validation" // Assessment: // - Magic number 0 could be a constant // - Error message could be a constant // - But... it's simple enough. Skip refactoring. // If we had multiple validations: const VALIDATION_ERRORS = { NEGATIVE_AMOUNT: 'Amount must be positive', ZERO_AMOUNT: 'Amount must be greater than zero', EXCEEDS_LIMIT: 'Amount exceeds maximum limit', } as const; const MAX_PAYMENT_AMOUNT = 10000; export function processPayment(payment: Payment): PaymentResult { if (payment.amount < 0) { return { success: false, error: VALIDATION_ERRORS.NEGATIVE_AMOUNT }; } if (payment.amount === 0) { return { success: false, error: VALIDATION_ERRORS.ZERO_AMOUNT }; } if (payment.amount > MAX_PAYMENT_AMOUNT) { return { success: false, error: VALIDATION_ERRORS.EXCEEDS_LIMIT }; } return { success: true }; } ``` ## Complete TDD Session Example ```typescript // === CYCLE 1: Negative amounts === // RED: Write failing test it('should reject negative amounts', () => { const payment = getMockPayment({ amount: -100 }); const result = processPayment(payment); expect(result.success).toBe(false); }); // RUN: ❌ FAIL - processPayment is not defined // GREEN: Minimal implementation function processPayment(payment: Payment): PaymentResult { if (payment.amount < 0) return { success: false }; return { success: true }; } // RUN: ✅ PASS // REFACTOR: Assess - simple enough, skip // COMMIT: "feat: reject negative payment amounts" // === CYCLE 2: Zero amounts === // RED: Write failing test it('should reject zero amounts', () => { const payment = getMockPayment({ amount: 0 }); const result = processPayment(payment); expect(result.success).toBe(false); }); // RUN: ❌ FAIL - expected false, got true // GREEN: Add zero check function processPayment(payment: Payment): PaymentResult { if (payment.amount <= 0) return { success: false }; return { success: true }; } // RUN: ✅ PASS // REFACTOR: Assess - still simple, skip // COMMIT: "feat: reject zero payment amounts" // === CYCLE 3: Maximum amount === // RED: Write failing test it('should reject amounts over 10000', () => { const payment = getMockPayment({ amount: 10001 }); const result = processPayment(payment); expect(result.success).toBe(false); }); // RUN: ❌ FAIL - expected false, got true // GREEN: Add max check function processPayment(payment: Payment): PaymentResult { if (payment.amount <= 0) return { success: false }; if (payment.amount > 10000) return { success: false }; return { success: true }; } // RUN: ✅ PASS // REFACTOR: Now we have magic number 10000 // COMMIT first: "feat: reject payments over maximum" // Then refactor: const MAX_PAYMENT_AMOUNT = 10000; function processPayment(payment: Payment): PaymentResult { if (payment.amount <= 0) return { success: false }; if (payment.amount > MAX_PAYMENT_AMOUNT) return { success: false }; return { success: true }; } // RUN: ✅ PASS // COMMIT: "refactor: extract max payment constant" ``` ## Quality Gates Before committing, verify: ```bash # 1. All tests pass npm test # or pytest, cargo test # 2. Coverage meets threshold npm test -- --coverage # Verify: 80%+ overall # 3. Type checking passes npm run typecheck # or mypy, cargo check # 4. Linting passes npm run lint # or ruff, cargo clippy # 5. No secrets in code git diff --staged | grep -i "password\|secret\|api.key" ``` ## Coverage Verification **CRITICAL: Never trust claimed coverage - always verify.** ```bash # Python pytest --cov=src --cov-report=term-missing --cov-fail-under=80 # TypeScript npm test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80}}' # Rust cargo tarpaulin --fail-under 80 ``` ### Interpreting Coverage | Metric | Meaning | Target | |--------|---------|--------| | Lines | % of lines executed | 80%+ | | Branches | % of if/else paths taken | 80%+ | | Functions | % of functions called | 80%+ | | Statements | % of statements executed | 80%+ | ### Coverage Exceptions (Document!) ```python # pragma: no cover - Reason: Debug utility only used in development def debug_print(data): print(json.dumps(data, indent=2)) ``` ```typescript /* istanbul ignore next -- @preserve Reason: Error boundary for React */ if (process.env.NODE_ENV === 'development') { console.error(error); } ``` ## Anti-Patterns ### Writing Test After Code ``` ❌ WRONG: 1. Write all production code 2. Write tests to cover it 3. "I have 80% coverage!" ✅ CORRECT: 1. Write failing test 2. Write code to pass 3. Repeat until feature complete ``` ### Testing Implementation ```typescript // ❌ BAD: Tests implementation detail it('should call validatePayment', () => { const spy = jest.spyOn(service, 'validatePayment'); processPayment(payment); expect(spy).toHaveBeenCalled(); }); // ✅ GOOD: Tests behavior it('should reject invalid payments', () => { const payment = getMockPayment({ amount: -1 }); const result = processPayment(payment); expect(result.success).toBe(false); }); ``` ### Writing Too Many Tests at Once ``` ❌ WRONG: 1. Write 10 tests 2. Implement everything 3. All tests pass ✅ CORRECT: 1. Write 1 test 2. Make it pass 3. Refactor if needed 4. Repeat ``` ### Skipping Refactor Assessment ``` ❌ WRONG: 1. Test passes 2. Immediately write next test 3. Code becomes messy ✅ CORRECT: 1. Test passes 2. Ask: "Should I refactor?" 3. If yes: commit, then refactor 4. If no: continue ``` ## Commit Messages Follow this pattern: ```bash # After GREEN (feature added) git commit -m "feat: add payment amount validation" # After REFACTOR git commit -m "refactor: extract payment validation constants" # Test-only changes git commit -m "test: add edge cases for payment validation" # Bug fixes (driven by failing test) git commit -m "fix: handle null payment amount" ```