test-driven-development
Test-Driven Development (TDD)
Overview
Write the test first. Watch it fail. Then write the code.
TDD is not about testing - it's about design. Writing tests first forces you to think about the interface before the implementation.
When to Use
- Implementing any new function, method, or feature
- Asked to "write code now, add tests later"
- Fixing a bug (write test that reproduces it first)
- Refactoring existing code
The Iron Rule
NEVER write production code without a failing test first.
No exceptions:
- Not for "it's a simple function"
- Not for "we'll add tests later"
- Not for "I already know how to implement it"
- Not for "tests slow me down"
The TDD Cycle
RED → GREEN → REFACTOR → REPEAT
- RED: Write a failing test
- GREEN: Write minimal code to pass
- REFACTOR: Clean up while tests pass
- REPEAT: Next test case
Detection: Code-First Smell
If you're about to write implementation without a test, STOP:
// ❌ VIOLATION: Implementation first
function calculateDiscount(total: number): number {
return total > 100 ? total * 0.1 : 0;
}
// "We'll add tests later if needed"
// ✅ CORRECT: Test first
describe('calculateDiscount', () => {
it('returns 10% discount for orders over $100', () => {
expect(calculateDiscount(150)).toBe(15);
});
it('returns 0 for orders $100 or less', () => {
expect(calculateDiscount(100)).toBe(0);
expect(calculateDiscount(50)).toBe(0);
});
});
// NOW write the implementation
function calculateDiscount(total: number): number {
return total > 100 ? total * 0.1 : 0;
}
Why Test-First Matters
| Test-After | Test-First |
|---|---|
| Tests verify what code does | Tests define what code should do |
| Implementation drives design | Requirements drive design |
| Tests often skipped | Tests always exist |
| Hard to test = poor design | Hard to test = caught early |
| "Does it work?" | "Is it right?" |
Pressure Resistance Protocol
1. "We'll Add Tests Later"
Pressure: "Just write the function, we can test it later"
Response: "Later" never comes. Tests written after are weaker and often skipped entirely.
Action: Write the test now. It takes 30 seconds.
2. "It's Too Simple to Test"
Pressure: "This function is trivial, testing is overkill"
Response: Simple functions grow complex. Simple bugs cause outages. Test everything.
Action: Write the test. Simple functions = simple tests.
3. "I Know How to Implement It"
Pressure: "I already have the solution in my head"
Response: TDD isn't about uncertainty - it's about capturing requirements as executable specs.
Action: Write the test first. Prove your mental model is correct.
4. "Tests Slow Me Down"
Pressure: "Writing tests takes extra time"
Response: Debugging takes 10x longer than testing. TDD is faster overall.
Action: The test you write now saves hours of debugging later.
Red Flags - STOP and Reconsider
- Writing any function without a test
- "Let me just get it working first"
- Implementation file open without test file
- Committing code without corresponding tests
- Tests that pass immediately (never saw them fail)
All of these mean: Stop. Write the test first.
Quick Reference
| Situation | TDD Response |
|---|---|
| New feature | Write failing test → implement |
| Bug fix | Write test that reproduces bug → fix |
| Refactor | Ensure tests exist → refactor → tests pass |
| "Tests later" | Tests now. Always now. |
Common Rationalizations (All Invalid)
| Excuse | Reality |
|---|---|
| "Tests slow me down" | Debugging slows you down more. |
| "It's too simple" | Simple tests are fast to write. |
| "I'll test later" | You won't. Test now. |
| "I know it works" | Prove it with a test. |
| "Tests are for QA" | Tests are for developers. |
| "Just a prototype" | Prototypes become production. Test them. |
The Bottom Line
Test first. Code second. No exceptions.
The test is the specification. The test is the documentation. The test is the safety net. Write it first, every time.
More from yanko-belov/code-craft
dont-repeat-yourself
Use when writing similar code in multiple places. Use when copy-pasting code. Use when making the same change in multiple locations.
84lazy-loading
Use when loading all data upfront. Use when initial page load is slow. Use when fetching data that might not be needed.
54keep-it-simple
Use when tempted to write clever code. Use when solution feels complex. Use when showing off skills instead of solving problems.
50separation-of-concerns
Use when component does too many things. Use when mixing data fetching, logic, and presentation. Use when code is hard to test.
42single-responsibility-principle
Use when creating or modifying classes, modules, or functions. Use when feeling pressure to add functionality to existing code. Use when class has multiple reasons to change.
37fail-fast
Use when handling errors. Use when tempted to catch and swallow exceptions. Use when returning default values to hide failures.
35