$npx -y skills add mvschwarz/openrig --skill test-driven-developmentUse when implementing any feature or bugfix, before writing implementation code
| 1 | # Test-Driven Development (TDD) |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | Write the test first. Watch it fail. Write minimal code to pass. |
| 6 | |
| 7 | **Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing. |
| 8 | |
| 9 | **Violating the letter of the rules is violating the spirit of the rules.** |
| 10 | |
| 11 | ## When to Use |
| 12 | |
| 13 | **Always:** |
| 14 | - New features |
| 15 | - Bug fixes |
| 16 | - Refactoring |
| 17 | - Behavior changes |
| 18 | |
| 19 | **Exceptions (ask your human partner):** |
| 20 | - Throwaway prototypes |
| 21 | - Generated code |
| 22 | - Configuration files |
| 23 | |
| 24 | Thinking "skip TDD just this once"? Stop. That's rationalization. |
| 25 | |
| 26 | ## The Iron Law |
| 27 | |
| 28 | ``` |
| 29 | NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST |
| 30 | ``` |
| 31 | |
| 32 | Write code before the test? Delete it. Start over. |
| 33 | |
| 34 | **No exceptions:** |
| 35 | - Don't keep it as "reference" |
| 36 | - Don't "adapt" it while writing tests |
| 37 | - Don't look at it |
| 38 | - Delete means delete |
| 39 | |
| 40 | Implement fresh from tests. Period. |
| 41 | |
| 42 | ## Red-Green-Refactor |
| 43 | |
| 44 | ```dot |
| 45 | digraph tdd_cycle { |
| 46 | rankdir=LR; |
| 47 | red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"]; |
| 48 | verify_red [label="Verify fails\ncorrectly", shape=diamond]; |
| 49 | green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"]; |
| 50 | verify_green [label="Verify passes\nAll green", shape=diamond]; |
| 51 | refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"]; |
| 52 | next [label="Next", shape=ellipse]; |
| 53 | |
| 54 | red -> verify_red; |
| 55 | verify_red -> green [label="yes"]; |
| 56 | verify_red -> red [label="wrong\nfailure"]; |
| 57 | green -> verify_green; |
| 58 | verify_green -> refactor [label="yes"]; |
| 59 | verify_green -> green [label="no"]; |
| 60 | refactor -> verify_green [label="stay\ngreen"]; |
| 61 | verify_green -> next; |
| 62 | next -> red; |
| 63 | } |
| 64 | ``` |
| 65 | |
| 66 | ### RED - Write Failing Test |
| 67 | |
| 68 | Write one minimal test showing what should happen. |
| 69 | |
| 70 | <Good> |
| 71 | ```typescript |
| 72 | test('retries failed operations 3 times', async () => { |
| 73 | let attempts = 0; |
| 74 | const operation = () => { |
| 75 | attempts++; |
| 76 | if (attempts < 3) throw new Error('fail'); |
| 77 | return 'success'; |
| 78 | }; |
| 79 | |
| 80 | const result = await retryOperation(operation); |
| 81 | |
| 82 | expect(result).toBe('success'); |
| 83 | expect(attempts).toBe(3); |
| 84 | }); |
| 85 | ``` |
| 86 | Clear name, tests real behavior, one thing |
| 87 | </Good> |
| 88 | |
| 89 | <Bad> |
| 90 | ```typescript |
| 91 | test('retry works', async () => { |
| 92 | const mock = jest.fn() |
| 93 | .mockRejectedValueOnce(new Error()) |
| 94 | .mockRejectedValueOnce(new Error()) |
| 95 | .mockResolvedValueOnce('success'); |
| 96 | await retryOperation(mock); |
| 97 | expect(mock).toHaveBeenCalledTimes(3); |
| 98 | }); |
| 99 | ``` |
| 100 | Vague name, tests mock not code |
| 101 | </Bad> |
| 102 | |
| 103 | **Requirements:** |
| 104 | - One behavior |
| 105 | - Clear name |
| 106 | - Real code (no mocks unless unavoidable) |
| 107 | |
| 108 | ### Verify RED - Watch It Fail |
| 109 | |
| 110 | **MANDATORY. Never skip.** |
| 111 | |
| 112 | ```bash |
| 113 | npm test path/to/test.test.ts |
| 114 | ``` |
| 115 | |
| 116 | Confirm: |
| 117 | - Test fails (not errors) |
| 118 | - Failure message is expected |
| 119 | - Fails because feature missing (not typos) |
| 120 | |
| 121 | **Test passes?** You're testing existing behavior. Fix test. |
| 122 | |
| 123 | **Test errors?** Fix error, re-run until it fails correctly. |
| 124 | |
| 125 | ### GREEN - Minimal Code |
| 126 | |
| 127 | Write simplest code to pass the test. |
| 128 | |
| 129 | <Good> |
| 130 | ```typescript |
| 131 | async function retryOperation<T>(fn: () => Promise<T>): Promise<T> { |
| 132 | for (let i = 0; i < 3; i++) { |
| 133 | try { |
| 134 | return await fn(); |
| 135 | } catch (e) { |
| 136 | if (i === 2) throw e; |
| 137 | } |
| 138 | } |
| 139 | throw new Error('unreachable'); |
| 140 | } |
| 141 | ``` |
| 142 | Just enough to pass |
| 143 | </Good> |
| 144 | |
| 145 | <Bad> |
| 146 | ```typescript |
| 147 | async function retryOperation<T>( |
| 148 | fn: () => Promise<T>, |
| 149 | options?: { |
| 150 | maxRetries?: number; |
| 151 | backoff?: 'linear' | 'exponential'; |
| 152 | onRetry?: (attempt: number) => void; |
| 153 | } |
| 154 | ): Promise<T> { |
| 155 | // YAGNI |
| 156 | } |
| 157 | ``` |
| 158 | Over-engineered |
| 159 | </Bad> |
| 160 | |
| 161 | Don't add features, refactor other code, or "improve" beyond the test. |
| 162 | |
| 163 | ### Verify GREEN - Watch It Pass |
| 164 | |
| 165 | **MANDATORY.** |
| 166 | |
| 167 | ```bash |
| 168 | npm test path/to/test.test.ts |
| 169 | ``` |
| 170 | |
| 171 | Confirm: |
| 172 | - Test passes |
| 173 | - Other tests still pass |
| 174 | - Output pristine (no errors, warnings) |
| 175 | |
| 176 | **Test fails?** Fix code, not test. |
| 177 | |
| 178 | **Other tests fail?** Fix now. |
| 179 | |
| 180 | ### REFACTOR - Clean Up |
| 181 | |
| 182 | After green only: |
| 183 | - Remove duplication |
| 184 | - Improve names |
| 185 | - Extract helpers |
| 186 | |
| 187 | Keep tests green. Don't add behavior. |
| 188 | |
| 189 | ### Repeat |
| 190 | |
| 191 | Next failing test for next feature. |
| 192 | |
| 193 | ## Good Tests |
| 194 | |
| 195 | | Quality | Good | Bad | |
| 196 | |---------|------|-----| |
| 197 | | **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` | |
| 198 | | **Clear** | Name describes behavior | `test('test1')` | |
| 199 | | **Shows intent** | Demonstrates desired API | Obscures what code should do | |
| 200 | |
| 201 | ## Why Order Matters |
| 202 | |
| 203 | **"I'll write tests after to verify it works"** |
| 204 | |
| 205 | Tests written after code pass immediately. Passing immediately proves nothing: |
| 206 | - Might test wrong thing |
| 207 | - Might test implementation, not behavior |
| 208 | - Might miss edge cases you forgot |
| 209 | - You never saw it catch the bug |
| 210 | |
| 211 | Test-first forces you to see the test |