Let’s Talk About TDD, Baby!

Elliot Chance
7 min readJan 30, 2016

Test Driven Development has been around for longer than you probably think, around 2003. Not that far back you say? Well actually TDD is just one component of the Extreme Programming practice that was first published back in 1999. These days TDD is what most people refer to it as since it’s used in combination with a variety of other practices to build a workflow that best suits the team. At least, in theory.

There are a lot of general misconceptions about what people may think, or have heard about Test Driven Development. I’ll bust a few common ones right now:

  • TDD means you have to write all the tests before you can begin coding. Actually you are only writing one test before you start coding.
  • TDD is so much more work. Bugs that come back after a release are much more work as well. Whether you practice TDD or not your software should be tested. TDD simply provides a test-as-you-go workflow that means you spend even less time at the end tying to bolt on tests.
  • I don’t do/need tests. Software engineer probably isn’t the career for you then.

TDD is the repetition of three steps:

  1. Test Fails: Write a test that fails.
  2. Test Passes: Make a change to the code to make the test pass.
  3. Refactor: Perform any cleanup or refactoring.

Immediately this should seem counter-intuitive. Why would you wanta failed test? I’ve read many articles that like to start by diving into the theory; this feels a lot like somebody that already understands the principles trying to explain everything in one go. Instead I will start with the code, explaining the decisions along the way with the remaining details in the conclusion looking back on those decisions.

Alright, enough talk, show me!

Fizz buzz

Some of you may have heard of Fizz buzz. It’s a very simple algorithm that works well for a first introduction of TDD. It works by translating an input number with the following rules:

  1. If the number is divisible by 3 we should return “Fizz”.
  2. If the number is divisible by 5 we should return “Buzz”.
  3. If the number is divisible by 15 we should return “FizzBuzz”.
  4. If none of the rules above match we could return the number itself.

There are lots of ways to do this, perhaps you can even write the algorithm in one go and have the correct result straight away. This is not a test to see how good you are, it’s to demonstrate the workflow of solving this problem by practicing TDD.

I will write the code examples in Python, but you should be able to follow along with whatever your favourite language is.

1. Write a Test That Fails

It may seem odd or counterintuitive to write a test that you know will fail, but that’s a very important step. If your test does not fail initially, your test is wrong. The reason we want it to fail is we want to prove that there is a deficiency with the application, call it a bug if you like.

import unittestclass TestFizzBuzz(unittest.TestCase):
def test_3_is_fizz(self):
self.assertEqual(fizz_buzz(3), 'Fizz')
# This will run the unit tests
unittest.main()

Run it to confirm there is a failure. Failures also include compilation errors like in this case since the fizz_buzzfunction does not exist. I will truncate most of the output for brevity:

$ python fizzbuzz.py 
E
----------------------------------------------------------------------
Ran 1 test in 0.000sFAILED (errors=1)

In python each unit test run is represented by one character, a .is a success, an Erepresents an error (unable to compile) and Frepresents a failure (the result was not what we expected).

2. Make a Change So the Test Passes

Once the test proved there is a bug, we can now resolve that bug. We want to write alter the code to fix the test. Make the smallest possible change — remembering that the next test has to fail. If we do to much in this step we risk not being able to find a test that fails and continue.

The simplest solution is to return “Fizz”:

def fizz_buzz(number):
return 'Fizz'

Running the tests again is now successful.

python fizzbuzz.py 
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

3. Refactor

For this first round there is no way to make the current code cleaner. In fact, it’s quite common there is no more work needed here. It’s time to start thinking about the next test…

1. Write a Test That Fails

def test_5_is_buzz(self):
self.assertEqual(fizz_buzz(5), 'Buzz')

Confirm the failure before you continue.

2. Make a Change So the Test Passes

def fizz_buzz(number):
if number == 5:
return 'Buzz'
return 'Fizz'

Notice that we have used == 5 instead of % 5 == 0. This will allow use to create another test later. The goal is to generate as many failing tests as possible.

3. Refactor

Once again, there is nothing to be refactored here.

def test_15_is_fizzbuzz(self):
self.assertEqual(fizz_buzz(15), 'FizzBuzz')

1. Write a Test That Fails

def test_15_is_fizzbuzz(self):
self.assertEqual(fizz_buzz(15), 'FizzBuzz')

2. Make a Change So the Test Passes

def fizz_buzz(number):
if number == 15:
return 'FizzBuzz'
if number == 5:
return 'Buzz'
return 'Fizz'

3. Refactor

Still nothing is needed yet.

1. Write a Test That Fails

def test_1_is_1(self):
self.assertEqual(fizz_buzz(1), 1)

2. Make a Change So the Test Passes

def fizz_buzz(number):
if number == 15:
return 'FizzBuzz'
if number == 5:
return 'Buzz'
if number == 1:
return 1
return 'Fizz'

3. Refactor

Still nothing.

1. Write a Test That Fails

def test_2_is_2(self):
self.assertEqual(fizz_buzz(2), 2)

2. Make a Change So the Test Passes

def fizz_buzz(number):
if number == 15:
return 'FizzBuzz'
if number == 5:
return 'Buzz'
if number == 1 or number == 2:
return number
return 'Fizz'

3. Refactor

This is the first refactor so far. We recognise that these if statements can now be simplified:

def fizz_buzz(number):
if number == 15:
return 'FizzBuzz'
if number == 5:
return 'Buzz'
if number == 3:
return 'Fizz'
return number

Remember to always run the tests before continuing to make sure all the tests are still passing.

1. Write a Test That Fails

def test_6_is_fizz(self):
self.assertEqual(fizz_buzz(6), 'Fizz’)

2. Make a Change So the Test Passes

def fizz_buzz(number):
if number == 15:
return 'FizzBuzz'
if number == 5:
return 'Buzz'
if number % 3 == 0:
return 'Fizz'
return number

3. Refactor

Nothing this time.

1. Write a Test That Fails

def test_10_is_buzz(self):
self.assertEqual(fizz_buzz(10), 'Buzz')

2. Make a Change So the Test Passes

def fizz_buzz(number):
if number == 15:
return 'FizzBuzz'
if number % 5 == 0:
return 'Buzz'
if number % 3 == 0:
return 'Fizz'
return number

3. Refactor

Still nothing.

1. Write a Test That Fails

def test_30_is_fizzbuzz(self):
self.assertEqual(fizz_buzz(30), 'FizzBuzz’)

2. Make a Change So the Test Passes

def fizz_buzz(number):
if number % 15 == 0:
return 'FizzBuzz'
if number % 5 == 0:
return 'Buzz'
if number % 3 == 0:
return 'Fizz'
return number

3. Refactor

It would be possible to write the code to take advantage of the fact 5 and 3 are factors of 15, but the result code would actually be more complicated than the current solution. So we won’t do that.

1. Write a Test That Fails

There is no test that can be written that can break the rules of the original requirements. This is how we know that we’re finished. The final product looks like this:

import unittestdef fizz_buzz(number):
if number % 15 == 0:
return 'FizzBuzz'
if number % 5 == 0:
return 'Buzz'
if number % 3 == 0:
return 'Fizz'
return number
class TestFizzBuzz(unittest.TestCase):
def test_3_is_fizz(self):
self.assertEqual(fizz_buzz(3), 'Fizz')
def test_5_is_buzz(self):
self.assertEqual(fizz_buzz(5), 'Buzz')
def test_15_is_fizzbuzz(self):
self.assertEqual(fizz_buzz(15), 'FizzBuzz')
def test_1_is_1(self):
self.assertEqual(fizz_buzz(1), 1)
def test_2_is_2(self):
self.assertEqual(fizz_buzz(2), 2)
def test_6_is_fizz(self):
self.assertEqual(fizz_buzz(6), 'Fizz')
def test_10_is_buzz(self):
self.assertEqual(fizz_buzz(10), 'Buzz')
def test_30_is_fizzbuzz(self):
self.assertEqual(fizz_buzz(30), 'FizzBuzz')
# This will run the unit tests
unittest.main()

Just Let Me Write the Answer!

As you were following along you were probably maddened by the fact it took so many steps (8 rounds) to solve the answer you might have already known. This is a fair concern and is by far the biggest initial criticism. However, remember that this was intended to be easy to solve, what if the problem were much harder and couldn’t be simply worked out in your head? Is there any other process you could follow that would almost guarantee that the result would cover all the scenarios?

What we can say about the solution is…

  1. We’ve covered every case we can think of so there is a higher confidence in our solution.
  2. The solution can be refactored with a very high degree of confidence that the original specification will remain intact.
  3. There is a high chance this is the most simple solution to the problem.

When your requirements are constantly changing this becomes invaluable.

So What’s Next?

Either this has peaked your interest, or enraged you. In either case that’s a great reason to investigate why TDD makes you feel this way. There is tons of material on the web, specially the next step would be to try another TDD kata.

Originally published at http://elliot.land on January 30, 2016.

--

--

Elliot Chance

I’m a data nerd and TDD enthusiast originally from Sydney. Currently working for Uber in New York. My thoughts here are my own. 🤓 elliotchance@gmail.com