Test-Driven Development
Overview
Write the test first. Watch it fail. Write minimal code to pass.
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
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 first):
- Throwaway prototypes
- Generated code
- Configuration files
The Iron Law
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
Write code before the test? Delete it. Start over.
- 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.
Red-Green-Refactor
RED - Write Failing Test
Write one minimal test showing what should happen.
Requirements:
- One behavior per test
- Clear, descriptive name
- Real code (no mocks unless unavoidable)
Good:
test('rejects empty email', () => {
const result = submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
Bad:
test('form works', () => { // vague name
// tests 5 things at once // too many behaviors
});
Verify RED - Watch It Fail
MANDATORY. Never skip.
Run the test. Confirm:
- Test fails (not errors from typos)
- Failure message matches expectation
- Fails because the feature is missing
Test passes immediately? You're testing existing behavior. Fix the test.
GREEN - Minimal Code
Write the simplest code to make the test pass. Nothing more.
- Don't add features beyond the test
- Don't refactor other code
- Don't "improve" beyond what the test requires
Verify GREEN - Watch It Pass
MANDATORY.
Run the test. Confirm:
- Test passes
- All other tests still pass
- Output clean (no errors, warnings)
Test fails? Fix the code, not the test.
REFACTOR - Clean Up
After green only:
- Remove duplication
- Improve names
- Extract helpers
Keep tests green. Don't add behavior.
Repeat
Next failing test for next behavior.
Why Order Matters
| Excuse | Reality | |--------|---------| | "I'll write tests after" | Tests passing immediately prove nothing | | "Too simple to test" | Simple code breaks. Test takes 30 seconds. | | "Already manually tested" | Ad-hoc != systematic. No record, can't re-run. | | "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is tech debt. | | "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. | | "Need to explore first" | Fine. Throw away exploration, start with TDD. | | "Test hard = design unclear" | Listen to the test. Hard to test = hard to use. | | "TDD slows me down" | TDD is faster than debugging. Always. | | "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" | | "It's about spirit not ritual" | Tests-after are biased by implementation. You test what you built, not what's required. | | "This is different because..." | No it isn't. Delete code. Start over with TDD. |
Red Flags - STOP and Start Over
If any of these are true, you skipped TDD:
- Code written before test
- Test written after implementation
- Test passes immediately (never seen failing)
- Can't explain why test failed
- "I already manually tested it"
- "Tests after achieve the same purpose"
- "Keep as reference"
- "Just this once"
- "This is different because..."
All of these mean: Delete code. Start over with TDD.
Bug Fix Flow
- Write failing test reproducing the bug
- Watch it fail with the expected error
- Write minimal fix
- Watch it pass
- Run full suite - no regressions
Never fix bugs without a test.
When Stuck
| Problem | Solution | |---------|----------| | Don't know how to test | Write wished-for API. Write assertion first. | | Test too complicated | Design too complicated. Simplify the interface. | | Must mock everything | Code too coupled. Use dependency injection. | | Test setup huge | Extract helpers. Still complex? Simplify design. |
Testing Anti-Patterns
Watch for these:
- Testing mock behavior instead of real behavior - if your assertion is about the mock, you're testing the mock
- Test-only methods in production - move cleanup logic to test utilities
- Mocking without understanding - know what side effects matter before mocking
- Incomplete mocks - partial mock structures create silent failures
- Mock setup > 50% of test - you're probably mocking too much
Verification Checklist
Before marking work complete:
- [ ] Every new function/method has a test
- [ ] Watched each test fail before implementing
- [ ] Each test failed for expected reason (feature missing, not typo)
- [ ] Wrote minimal code to pass each test
- [ ] All tests pass
- [ ] Output clean (no errors, warnings)
- [ ] Tests use real code (mocks only if unavoidable)
- [ ] Edge cases and errors covered
Can't check all boxes? You skipped TDD. Start over.
Final Rule
Production code -> test exists and failed first
Otherwise -> not TDD
No exceptions without explicit permission.