A blog by Gary Bernhardt, Creator & Destroyer of Software

How I Started TDD

04 Nov 2009

This story is about the first code I ever wrote with proper TDD. I'd been doing test-first for several months, but I didn't understand the design aspect. Fortunately, Corey Haines wanted to learn Python, and I wanted to learn TDD, so we paired up at a Coding Dojo. It went something like this. [1]

Corey: Let's write a test.

def test_fib_of_0_is_0():
    assert fib(0) == 0

1 test failed; 0 tests passed.

Corey: Now let's make it pass.

Me: Well, we could iterate...

Corey: Why?

Me: Because it's fibonacci...

Corey: The test says it returns zero!

Me: Oh. Well, OK.

def fib(n):
    return 0

1 test passed.

Corey: Let's write another test.

def test_fib_of_1_is_1():
    assert fib(1) == 1

1 test failed; 1 test passed.

Corey: Now let's make it pass.

Me: OK, we need to recursively...

I stop myself. I know what this got me last time.

Me: We can check for which input we got.

Corey: We don't even need that.

def fib(n):
    return n

2 tests passed.

Corey: Let's write another test.

def test_fib_of_2_is_1():
    assert fib(2) == 1

1 test failed; 2 tests passed.

Corey: Now let's make it pass

I pause while I find the correct answer.

Me: Only the zero case is different.

def fib(n):
    if n == 0:
        return 0
    else:
        return 1

3 tests passed.

(I consider the implications of this. "Only the zero case is different." This is an inductive system, so it needs a basis case. Zero is only half of the basis case of a fibonacci sequence, but I never had to think about a basis case or recursion to write this code. The tests showed me what the code needed to do.)

Corey: Let's write another test.

def test_fib_of_3_is_2():
    assert fib(3) == 2

1 test failed; 3 tests passed.

Me: Another if?

Corey: Another if.

def fib(n):
    if n == 0:
        return 0
    elif n < 3:
        return 1
    else:
        return 2

4 tests passed.

Corey: Refactor!

Me: I don't know...

My brain hurts for a moment.

def fib(n):
    if n < 2:
        return n
    else:
        return n - 1

4 tests passed.

The full basis case is in place and we don't even need recursion yet. I'm surprised by how many cases we've written without needing recursion or iteration.

Corey: Another test.

def test_fib_of_4_is_3():
    assert fib(4) == 3

5 tests passed.

Me: It passed without changes. Is that OK?

Corey: Another test!

def test_fib_of_5_is_5():
    assert fib(5) == 5

1 test failed; 5 tests passed.

I think I can handle this now.

def fib(n):
    if n < 2:
        return n
    elif n == 5:
        return 5
    else:
        return n - 1

6 tests passed.

Corey: Refactor!

Me: Combine them into... recursion?

Corey: Combine them into recursion.

def fib(n):
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

6 tests passed.

This isn't a perfect example of TDD, but that's not the point. The first thing you need to understand is the rough process: write the smallest failing test you can; then write the smallest code to make it pass; then refactor without changing behavior.

After getting this lesson from Corey, I went off and TDDed a couple thousand lines of code with almost no outside feedback. I was doing it very poorly, and often became frustrated, but in retrospect it was still the best code I'd ever written.

It takes years to learn how to do this well, and consistently, across a wide variety of situations. I've been doing it for two years, and I still have non-trivial problems, but I can almost always move forward confidently.

Building software without TDD was crushingly stressful, but I couldn't see it at the time. It was only shown to me when I started working one test at a time, one line of code at a time, with verification that the entire system is working in less than two seconds.

([1] In reality, the Coding Dojo probably went only vaguely like this, and this isn't even the problem we solved, but that's not the point. This is what the first true TDD session always looks like.)