test-driven-development

Use when implementing any feature or bugfix, before writing implementation code

Test-Driven Development (TDD)

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.

The Iron Law

NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST

Write code before the test? Delete it. Start over. Do not keep it as "reference" — delete means delete.

Red-Green-Refactor

1. RED — Write a Failing Test

Write one minimal test for the specific behavior you're implementing.

  • One behavior per test
  • Name describes the behavior
  • Use real code, not mocks (unless unavoidable)

2. Verify RED — Watch It Fail

MANDATORY. Never skip.

Run the test and confirm:

  • It fails (not errors out)
  • The failure message is expected
  • It fails because the feature is missing, not due to a typo

Test passes immediately? You're testing existing behavior. Fix the test.

3. GREEN — Write Minimal Code

Write the simplest code that makes the test pass. No extra features. No "nice-to-haves." Don't refactor other code.

4. Verify GREEN — Watch It Pass

MANDATORY. Never skip.

Run the test and confirm:

  • Your test passes
  • All other tests still pass
  • No new errors or warnings

Test still fails? Fix the code, not the test.

5. REFACTOR — Clean Up

After green only: remove duplication, improve names, extract helpers. Keep tests green. Do not add behavior.

6. Repeat

Move to the next failing test.

Good Tests

QualityGoodBad
MinimalOne thing. "and" in name? Split it.test('validates email and domain and whitespace')
ClearName describes behaviortest('test1')
RealTests actual behaviorTests mock interactions

Bug Fix Example

Bug: Empty email accepted

// RED
test('rejects empty email', async () => {
  const result = await submitForm({ email: '' });
  expect(result.error).toBe('Email required');
});

// Verify RED: FAIL — expected 'Email required', got undefined

// GREEN
function submitForm(data: FormData) {
  if (!data.email?.trim()) return { error: 'Email required' };
  // ...
}

// Verify GREEN: PASS

Verification Checklist

Before marking work complete:

  • Every new function/method has a test written before the implementation
  • Watched each test fail before implementing
  • Each test failed for the expected reason (feature missing, not typo)
  • Wrote minimal code to pass each test
  • All tests pass with clean output
  • Edge cases covered

Red Flags — Stop and Start Over

RationalizationReality
"Too simple to test"Simple code breaks too. Test takes 30 seconds.
"I'll write tests after"Tests passing immediately prove nothing.
"Already manually tested"Ad-hoc ≠ systematic. You can't re-run it.
"Deleting X hours is wasteful"Sunk cost. Keeping unverified code is technical debt.
"TDD will slow me down"TDD is faster than debugging.
"Keep as reference, write tests first"You'll adapt it. That's testing after. Delete means delete.

Any of these? Delete code. Start over with TDD.

When Stuck

ProblemSolution
Don't know how to testWrite the wished-for API. Write the assertion first.
Test too complicatedDesign too complicated. Simplify the interface.
Must mock everythingCode too coupled. Use dependency injection.
Test setup hugeExtract helpers. Still complex? Simplify the design.