| name | tdd |
| description | Use when implementing any feature or bugfix, before writing implementation code. Enforces strict test-first development — no production code without a failing test. |
Test-Driven Development (TDD)
Overview
Write the test first. Watch it fail. Write minimal code to pass.
Core principle: Without watching the test fail first, the test's correctness is unknown — a passing test that was never seen failing might be vacuous.
Violating the letter of the rules is violating the spirit of the rules.
When to Use
Always:
- New features
- Bug fixes
- Refactoring
- Behavior changes
Exceptions (ask the human partner first):
- Throwaway prototypes
- Generated code
- Configuration files
The thought "skip TDD just this once" is rationalization — stop and reconsider.
The Iron Law
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
Write code before the test? Delete it. Start over.
No exceptions:
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete
Implement fresh from tests. Period.
The Cycle: RED -> VERIFY RED -> GREEN -> VERIFY GREEN -> REFACTOR
Every piece of production code follows this cycle. No shortcuts. No reordering.
Step 1: RED — Write the Failing Test
Write one minimal test that describes the behavior you want.
Requirements:
- One behavior per test
- Clear name that describes what should happen
- Real code paths (no mocks unless unavoidable)
- Write the assertion first, then work backward
# Test describes the WHAT, not the HOW
# Name states expected behavior clearly
# Tests real code, not mock behavior
Bad tests: vague names, multiple assertions testing unrelated things, mocking away the code under test.
Why mocks are last resort: a mock asserts what the test author thinks the dependency does. When the real dependency drifts, the mock keeps passing while production breaks.
Step 2: VERIFY RED — Watch It Fail
MANDATORY. Never skip.
Run the project's test command targeting your new test.
Confirm:
- Test FAILS (not errors from syntax or imports)
- Failure message matches your expectation
- Fails because the feature is MISSING, not because of typos
Test passes immediately? It is testing existing behavior — the test is wrong. Fix it.
Test errors instead of failing? Fix the error. Re-run until it fails correctly — a clean failure for the right reason.
Step 3: GREEN — Write Minimal Code
Write the simplest possible code to make the test pass. Nothing more.
- No extra features
- No "while I'm here" improvements
- No refactoring other code
- No options, parameters, or generalization the test doesn't require
If the test asks for one thing, implement one thing.
Step 4: VERIFY GREEN — Watch It Pass
MANDATORY.
Run the project's test command.
Confirm:
- The new test passes
- ALL other tests still pass
- Output is pristine (no errors, no warnings, no deprecation notices)
New test fails? Fix the implementation, not the test.
Other tests broke? Fix them now. Do not proceed with broken tests.
Step 5: REFACTOR — Clean Up (Tests Still Green)
Only after green:
- Remove duplication
- Improve names
- Extract helpers
- Simplify logic
Run the project's test command after every change. Stay green. If tests break during refactor, undo and try again.
Do NOT add behavior during refactor. Refactor changes structure, not functionality.
Step 6: REPEAT
Next failing test. Next behavior. Same cycle. No exceptions.
Delete-and-Restart Enforcement
Write code before the test? Delete it. Start over. No exceptions.
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't glance at it for "inspiration"
- Don't copy-paste it into a test
- Delete means DELETE
200 lines without a test? Delete 200 lines. 4 hours spent? Those hours are gone regardless. Two options remain:
- Delete and rewrite with TDD — high confidence, real tests
- Keep it and bolt on tests after — false confidence, fragile tests
Option 2 is not TDD. It is rationalization wearing a lab coat.
Common Rationalizations
Every excuse below is invalid. Every single one.
| Excuse | Reality |
|---|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. Write it. |
| "I'll test after" | Tests passing immediately prove nothing. The proof is lost. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" Different questions. |
| "Already manually tested" | Ad-hoc is not systematic. No record. Can't re-run. Can't trust. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. The time is gone. Keeping unverified code is debt. |
| "Keep as reference" | Adaptation creeps in — that's testing-after with extra steps. Delete means delete. |
| "Need to explore first" | Fine. Explore. Then throw away ALL exploration code and start with TDD. |
| "Test is hard = design unclear" | Listen to the test. Hard to test = hard to use. Fix the design. |
| "TDD will slow me down" | TDD is faster than debugging. The time gets paid either way — upfront or in production. |
| "Manual test is faster" | Manual doesn't prove edge cases. Manual means re-testing every change forever. |
| "Existing code has no tests" | Add tests for the code being touched. No excuse. |
| "This is different because..." | It's not. Delete the code. Start over with TDD. |
Why Order Matters
"I'll write tests after to verify it works"
Tests written after code pass immediately. Passing immediately proves nothing:
- Might test the wrong thing
- Might test implementation details, not behavior
- Might miss edge cases that were forgotten
- The test never caught the bug
Test-first forces seeing the test fail, proving it actually tests something real.
"Tests after achieve the same goals — it's spirit not ritual"
No. Tests-after are biased by the implementation already in front of them. They verify what was built, not what's required. They check remembered edge cases, not discovered ones.
Tests-first force edge case discovery BEFORE implementing. Tests-after assume nothing was forgotten — that assumption rarely holds.
"Deleting X hours of work is wasteful"
Sunk cost fallacy. The time is already spent. Two options remain:
- Delete and rewrite with TDD (X more hours, high confidence)
- Keep it and add tests after (30 min, low confidence, likely bugs later)
The "waste" is keeping code that cannot be trusted.
"TDD is dogmatic, being pragmatic means adapting"
TDD IS pragmatic:
- Finds bugs before commit (faster than debugging in production)
- Prevents regressions (tests catch breaks immediately)
- Documents behavior (tests show how to use the code)
- Enables fearless refactoring (change freely, tests catch breaks)
"Pragmatic" shortcuts = debugging in production = slower.
Red Flags — STOP and Start Over
Stop immediately on any of these. Delete the code. Restart with TDD.
- Writing production code before a test exists
- Writing tests after implementation is done
- A new test passes immediately without writing new code
- Unable to explain why a test failed
- Planning to add tests "later"
- Rationalizing "just this once"
- "Already manually tested it"
- Claiming "tests after achieve the same purpose"
- Arguing "it's about spirit not ritual"
- Keeping code as "reference" or "adapting existing code"
- "Already spent X hours, deleting is wasteful"
- Claiming "TDD is dogmatic, this is being pragmatic"
- Thinking "this is different because..."
- Expressing satisfaction or confidence before running verification
All of these mean the same thing: Delete the code. Start over with TDD.
No negotiation. No partial credit. Delete and restart.
Good Tests
| Quality | Good | Bad |
|---|
| Minimal | Tests one thing. "and" in name? Split it. | test('validates email and domain and whitespace') |
| Clear | Name describes expected behavior | test('test1'), test('it works') |
| Intent | Demonstrates the desired API | Obscures what code should do |
| Real | Tests actual code paths | Tests mock behavior instead of real behavior |
| Focused | Assertion matches the test name | Asserts unrelated side effects |
When Stuck
| Problem | Solution |
|---|
| Don't know how to test | Write the API the test wishes existed. Write the assertion first. Ask the human partner. |
| Test too complicated | Design too complicated. Simplify the interface. |
| Must mock everything | Code too coupled. Use dependency injection. Reduce coupling. |
| Test setup is huge | Extract test helpers. Still complex? Simplify the design. |
| Can't isolate the unit | Break dependencies. Inject them. Make the boundary explicit. |
Debugging Integration
Bug found? Write a failing test that reproduces it FIRST. Then follow the TDD cycle. The test proves the fix works AND prevents regression.
Never fix bugs without a test. The test is the proof. Without proof, the fix is a guess.
Verification Checklist
Before marking any task complete, confirm ALL of these:
Any unchecked box means TDD was skipped — start over.
Final Rule
Production code -> test exists and failed first
Otherwise -> not TDD
No exceptions without explicit permission from the human partner.