Simyl
simylflow
Course Home
Module 2: Technical Practices
Lesson 1 of 5
14 min

Test-Driven Development

Write the test first, let it drive design, and build confidence with every keystroke.

1The Red-Green-Refactor Cycle

Test-Driven Development is simple to describe and hard to master:

  1. Red: Write a failing test for the next small piece of functionality
  2. Green: Write the minimum code to make the test pass
  3. Refactor: Clean up the code while keeping tests green

That's it. Three steps, repeated hundreds of times a day.

The order matters. You write the test first, before any production code exists. This feels backwards at first. But it changes everything:

  • You think about how the code will be used before how it will be implemented
  • You get immediate feedback on whether your code works
  • You build a safety net that enables fearless change
  • You create executable documentation of what the code does

TDD Is Design

TDD is a design technique disguised as a testing technique. The tests aren't the point—the thinking that produces them is.

2Writing the Test First

The hardest part of TDD is learning to write tests for code that doesn't exist yet. Here's how:

Start with the interface, not the implementation. What should the code look like when it's done? Write a test that uses it that way.

Start small. Don't try to test everything at once. Test the simplest possible case first.

Make it fail for the right reason. The test should fail because the functionality doesn't exist, not because you made a syntax error.

Use test names as documentation. The test name should describe what behavior you're testing: "returns empty list when no items match filter" not "test1."

Writing the test first forces you to think about:

  • What inputs does this code need?
  • What outputs should it produce?
  • What happens in edge cases?
  • How will other code call this?

These are design questions. By answering them before you write the code, you make better design decisions.

Good Test-First Approach

Need a function to calculate shipping cost. Write a test: 'shipping to California with $50 order is $5.' Then write just enough code to make it pass. Then test the next case.

Test-After Trap

Write the entire shipping calculation with all edge cases. Then try to write tests. Discover the code is hard to test because it wasn't designed for testability. Write messy tests or skip testing.

3Making It Pass (The Right Way)

Once you have a failing test, make it pass. But here's the discipline: write the minimum code to pass the test, no more.

This means:

  • If a constant makes the test pass, use a constant
  • If hard-coding a return value works, hard-code it
  • Don't generalize until you have tests that require generalization

This sounds ridiculous. "Just return 5?" Yes, if that makes the test pass. Then write another test that forces you to do real computation.

Why? Because:

  • You build up functionality in tiny, verified steps
  • You never write code you don't need
  • You discover the design incrementally
  • Every line of code is justified by a test

The magic happens when you triangulate. First test: return 5. Second test (different input): now you have to compute. Third test: now you have to handle edge cases. Each test pushes you toward a real implementation.

If you can make the test pass by writing 'return 5', write 'return 5'. Then write another test that breaks it. Let the tests drive you toward real code.

4The Refactoring Step

After the test passes, clean up. This is not optional.

The refactoring step is where you:

  • Remove duplication (DRY: Don't Repeat Yourself)
  • Improve names (make the code read like prose)
  • Simplify logic (reduce cognitive load)
  • Reorganize (move code to where it belongs)

The key: keep the tests passing throughout. Refactor in tiny steps, running tests after each change. If tests fail, you know exactly what broke.

This is where TDD pays off. Without tests, refactoring is terrifying—you might break something. With tests, refactoring is routine—you'll know immediately if you break something.

Skipping the refactoring step leads to what Kent Beck calls "quick green excuses." The code works, but it's messy. The mess accumulates. Soon the codebase is hard to work with, and you've lost the benefit of TDD.

5TDD Myths Debunked

Myth: TDD is slower. Reality: TDD seems slower at first because you're writing more code. But you spend less time debugging, less time in the debugger, less time fixing production bugs. Over the life of the codebase, TDD is faster.

Myth: TDD leads to over-testing. Reality: TDD leads to exactly the tests you need—no more, no less. You test what you build. Test-after often leads to missing tests (you forget edge cases) or redundant tests (you test the same thing multiple ways).

Myth: TDD doesn't work for [my domain]. Reality: TDD works for any code that can be tested. If your code can't be tested, that's a design problem. TDD forces you to write testable code, which is better code.

Myth: We don't have time for TDD. Reality: You don't have time not to. Bugs are expensive. Debugging is expensive. Production incidents are expensive. TDD is an investment that pays off.

Non-Negotiable

In XP, TDD isn't a nice-to-have. It's a non-negotiable practice. Without tests, every other practice becomes harder or impossible.

Key Takeaways
  • TDD follows the red-green-refactor cycle: fail, pass, clean up
  • Write the test first—it's a design technique, not just testing
  • Make tests pass with the minimum code needed, then iterate
  • Never skip the refactoring step—it's where quality is built
  • TDD is faster in the long run despite seeming slower at first
Common Pitfalls to Avoid
  • Writing tests after the code (you lose the design benefit)
  • Skipping the refactoring step (code quality degrades)
  • Testing implementation details instead of behavior
  • Writing too much code before the next test

Practice Exercises