Test-Driven Development

What Is TDD and Why It Works

Test-Driven Development flips the conventional workflow — write the test first, then make it pass.

TDD Defined

Test-Driven Development (TDD) is a development practice with one deceptively simple rule: write the test before you write the code.

The cycle:

  1. Write a test that describes a requirement (it will fail — the code doesn't exist yet)
  2. Write the minimum code to make the test pass
  3. Refactor the code without changing behavior (tests verify you didn't break anything)
  4. Repeat

Why Write Tests First?

When you write tests after implementation, you unconsciously test what the code does rather than what it should do. The tests are biased toward the implementation.

When you write tests first, you are forced to think about requirements before implementation. This produces fundamentally different code:

  • Testable by design — if you can't write a test for it, the code is probably too complex
  • Minimal — you only write code that a test demands; no speculative features
  • Behavior-focused — tests capture intent, not implementation

The Three Laws of TDD

Robert Martin (Uncle Bob) codified TDD into three laws:

  1. You may not write production code until you have a failing test.
  2. You may not write more of a test than is sufficient to fail.
  3. You may not write more production code than is sufficient to pass the test.

These laws enforce tiny cycles. Each cycle is a few minutes. At any given moment, you are no more than a few minutes from a passing state.

TDD vs Testing After

TDDTesting After
When tests are writtenBefore codeAfter code
Test biasBehavioralImplementation
Code design feedbackImmediateAfter the fact
Coverage guaranteeHigh (every feature has a test)Varies
Discovery of unclear requirementsDuring test writingDuring implementation

When TDD Shines

TDD is most powerful for:

  • Business logic — price calculations, validation rules, permission systems
  • Utility functions — data transformations, formatters, parsers
  • API routes — input validation, response format, error handling
  • Domain models — the core rules of your application

TDD is less natural for:

  • UI layout and visual design — hard to test visually
  • Exploratory prototyping — when you don't know what you're building yet
  • Third-party integrations — you're testing their behavior, not yours

TDD with AI Coding Tools

TDD and AI tools form a powerful combination:

  1. You write the test (defines the requirement precisely)
  2. AI implements the code (generates something that passes)
  3. Tests verify correctness automatically

The test you write becomes the specification for the AI. The more precise and comprehensive your tests, the better the AI's implementation.

Key Takeaways

  • TDD writes tests before code — this forces clarity about requirements before implementation begins
  • The three laws: no production code without a failing test, test only enough to fail, code only enough to pass
  • TDD produces testable, minimal, behavior-focused code by design
  • TDD is most valuable for business logic, utility functions, and API routes — less natural for UI and exploration
  • AI tools + TDD: you write the tests (the specification), the AI writes the implementation

Example

typescript
// TDD workflow: each test drives a new feature

// Step 1: Write failing test
it('validates minimum password length', () => {
  expect(validatePassword('short')).toContain('at least 8 characters');
});
// Test fails — validatePassword doesn't exist yet

// Step 2: Write minimum code to pass
function validatePassword(password: string): string[] {
  const errors: string[] = [];
  if (password.length < 8) errors.push('at least 8 characters');
  return errors;
}
// Test passes

// Step 3: Write next failing test
it('requires an uppercase letter', () => {
  expect(validatePassword('alllower1')).toContain('one uppercase letter');
});
// Test fails — implement next requirement...

// Step 4: Implement
function validatePassword(password: string): string[] {
  const errors: string[] = [];
  if (password.length < 8) errors.push('at least 8 characters');
  if (!/[A-Z]/.test(password)) errors.push('one uppercase letter');
  return errors;
}
// Continue cycle...
Try it yourself — TYPESCRIPT