$npx -y skills add jmxt3/gitscape.ai --skill test-driven-developmentDrives development with tests. Use when implementing any logic, fixing any bug, or changing any behavior. Use when you need to prove that code works, when a bug report arrives, or when you're about to modify existing functionality.
| 1 | # Test-Driven Development |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | Write a failing test before writing the code that makes it pass. For bug fixes, reproduce the bug with a test before attempting a fix. Tests are proof — "seems right" is not done. A codebase with good tests is an AI agent's superpower; a codebase without tests is a liability. |
| 6 | |
| 7 | ## When to Use |
| 8 | |
| 9 | - Implementing any new logic or behavior |
| 10 | - Fixing any bug (the Prove-It Pattern) |
| 11 | - Modifying existing functionality |
| 12 | - Adding edge case handling |
| 13 | - Any change that could break existing behavior |
| 14 | |
| 15 | **When NOT to use:** Pure configuration changes, documentation updates, or static content changes that have no behavioral impact. |
| 16 | |
| 17 | **Related:** For browser-based changes, combine TDD with runtime verification using Chrome DevTools MCP — see the Browser Testing section below. |
| 18 | |
| 19 | ## The TDD Cycle |
| 20 | |
| 21 | ``` |
| 22 | RED GREEN REFACTOR |
| 23 | Write a test Write minimal code Clean up the |
| 24 | that fails ──→ to make it pass ──→ implementation ──→ (repeat) |
| 25 | │ │ │ |
| 26 | ▼ ▼ ▼ |
| 27 | Test FAILS Test PASSES Tests still PASS |
| 28 | ``` |
| 29 | |
| 30 | ### Step 1: RED — Write a Failing Test |
| 31 | |
| 32 | Write the test first. It must fail. A test that passes immediately proves nothing. |
| 33 | |
| 34 | ```typescript |
| 35 | // RED: This test fails because createTask doesn't exist yet |
| 36 | describe('TaskService', () => { |
| 37 | it('creates a task with title and default status', async () => { |
| 38 | const task = await taskService.createTask({ title: 'Buy groceries' }); |
| 39 | |
| 40 | expect(task.id).toBeDefined(); |
| 41 | expect(task.title).toBe('Buy groceries'); |
| 42 | expect(task.status).toBe('pending'); |
| 43 | expect(task.createdAt).toBeInstanceOf(Date); |
| 44 | }); |
| 45 | }); |
| 46 | ``` |
| 47 | |
| 48 | ### Step 2: GREEN — Make It Pass |
| 49 | |
| 50 | Write the minimum code to make the test pass. Don't over-engineer: |
| 51 | |
| 52 | ```typescript |
| 53 | // GREEN: Minimal implementation |
| 54 | export async function createTask(input: { title: string }): Promise<Task> { |
| 55 | const task = { |
| 56 | id: generateId(), |
| 57 | title: input.title, |
| 58 | status: 'pending' as const, |
| 59 | createdAt: new Date(), |
| 60 | }; |
| 61 | await db.tasks.insert(task); |
| 62 | return task; |
| 63 | } |
| 64 | ``` |
| 65 | |
| 66 | ### Step 3: REFACTOR — Clean Up |
| 67 | |
| 68 | With tests green, improve the code without changing behavior: |
| 69 | |
| 70 | - Extract shared logic |
| 71 | - Improve naming |
| 72 | - Remove duplication |
| 73 | - Optimize if necessary |
| 74 | |
| 75 | Run tests after every refactor step to confirm nothing broke. |
| 76 | |
| 77 | ## The Prove-It Pattern (Bug Fixes) |
| 78 | |
| 79 | When a bug is reported, **do not start by trying to fix it.** Start by writing a test that reproduces it. |
| 80 | |
| 81 | ``` |
| 82 | Bug report arrives |
| 83 | │ |
| 84 | ▼ |
| 85 | Write a test that demonstrates the bug |
| 86 | │ |
| 87 | ▼ |
| 88 | Test FAILS (confirming the bug exists) |
| 89 | │ |
| 90 | ▼ |
| 91 | Implement the fix |
| 92 | │ |
| 93 | ▼ |
| 94 | Test PASSES (proving the fix works) |
| 95 | │ |
| 96 | ▼ |
| 97 | Run full test suite (no regressions) |
| 98 | ``` |
| 99 | |
| 100 | **Example:** |
| 101 | |
| 102 | ```typescript |
| 103 | // Bug: "Completing a task doesn't update the completedAt timestamp" |
| 104 | |
| 105 | // Step 1: Write the reproduction test (it should FAIL) |
| 106 | it('sets completedAt when task is completed', async () => { |
| 107 | const task = await taskService.createTask({ title: 'Test' }); |
| 108 | const completed = await taskService.completeTask(task.id); |
| 109 | |
| 110 | expect(completed.status).toBe('completed'); |
| 111 | expect(completed.completedAt).toBeInstanceOf(Date); // This fails → bug confirmed |
| 112 | }); |
| 113 | |
| 114 | // Step 2: Fix the bug |
| 115 | export async function completeTask(id: string): Promise<Task> { |
| 116 | return db.tasks.update(id, { |
| 117 | status: 'completed', |
| 118 | completedAt: new Date(), // This was missing |
| 119 | }); |
| 120 | } |
| 121 | |
| 122 | // Step 3: Test passes → bug fixed, regression guarded |
| 123 | ``` |
| 124 | |
| 125 | ## The Test Pyramid |
| 126 | |
| 127 | Invest testing effort according to the pyramid — most tests should be small and fast, with progressively fewer tests at higher levels: |
| 128 | |
| 129 | ``` |
| 130 | ╱╲ |
| 131 | ╱ ╲ E2E Tests (~5%) |
| 132 | ╱ ╲ Full user flows, real browser |
| 133 | ╱──────╲ |
| 134 | ╱ ╲ Integration Tests (~15%) |
| 135 | ╱ ╲ Component interactions, API boundaries |
| 136 | ╱────────────╲ |
| 137 | ╱ ╲ Unit Tests (~80%) |
| 138 | ╱ ╲ Pure logic, isolated, milliseconds each |
| 139 | ╱──────────────────╲ |
| 140 | ``` |
| 141 | |
| 142 | **The Beyonce Rule:** If you liked it, you should have put a test on it. Infrastructure changes, refactoring, and migrations are not responsible for catching your bugs — your tests are. If a change breaks your code and you didn't have a test for it, that's on you. |
| 143 | |
| 144 | ### Test Sizes (Resource Model) |
| 145 | |
| 146 | Beyond the pyramid levels, classify tests by what resources they consume: |
| 147 | |
| 148 | | Size | Constraints | Speed | Example | |
| 149 | |------|------------|-------|---------| |
| 150 | | **Small** | Single process, no I/O, no network, no database | Milliseconds | Pure function tests, data transforms | |
| 151 | | **Medium** | Multi-process OK, localhost only, no external |