mokacoding

unit and acceptance testing, automation, productivity

A real-world example of TDD catching bugs

I published a video tutorial showing how to implement the infamous FizzBuzz using Test-Driven Development, an example coming from my book Test-Driven Development in Swift.

Aside from the tutorial, what's truly interesting in the video is that it shows how fast TDD can reveal a mistake and help you learn from it.

How TDD reveals coding mistakes

After completing the implementation, I decided to refactor the ugly code I wrote to make the test pass as soon as possible into something I liked more.

I went from this:

func fizzBuzz(_ input: Int) -> String {
    let divisibleBy3 = input % 3 == 0
    let divisibleBy5 = input % 5 == 0

    if divisibleBy3 && divisibleBy5 {
        return "fizz-buzz"
    } else if divisibleBy3 {
        return "fizz"
    } else if divisibleBy5 {
        return "buzz"
    } else {
        return "\(input)"
    }
}

To this:

func fizzBuzz(_ input: Int) -> String {
    let divisibleBy3 = input % 3 == 0
    let divisibleBy5 = input % 5 == 0

    switch (divisibleBy3, divisibleBy5) {
    case (false, true): return "fizz"
    case (true, false): return "buzz"
    case (true, true): return "fizz-buzz"
    case (false, false): return "\(input)"
    }
}

Can you spot the error I made? The tests revealed it as soon as I run them.

In my copy-pasta, I used the code to check for "fizz" to print "buzz" and vice versa.

How long do you think it would have taken me to notice that mistake if I'd been relying solely on manual testing? More time for sure, as I would have had to manually launch the app or script that uses fizzBuzz(_:) feed it an input like 3 and notice the output was not correct. Apart from the overhead of launching the app and navigating its UI, which might be negligible in some cases, manually verifying code uses our biological hardware, which is liable to all sorts of distractions and hiccups.

How TDD reveals incorrect mental models

That refactoring mistake is not the only one I made in the screencast. Earlier on, the tests failed in a way that I was not expecting.

I the production code was in this incomplete state:

func fizzBuzz(_ input: Int) -> String {
    let divisibleBy3 = input % 3 == 0

    if divisibleBy3 {
    } else if divisibleBy3 {
        return "fizz"
    } else {
        return "buzz"
    }
}

I had just added a test for the "fizz-buzz" scenario:

func testDivisibleBy15ReturnsFizzBuzz() {
   XCTAssertEqual(fizzBuzz(15), "fizz-buzz")
}

Looking at the code, I expected it to return "buzz" and the test to fail. Can you see where I was wrong?

The test did fail indeed, but the value it received was "fizz" instead. I didn't realize that because 15 is divisible by both 3 and 5, the code would have gone in the divisibleBy3 == true branch.

Always guess what the tests will do before running them. When the result you get is not what you expect, you can update your mental representation of the system.

Here's the full video:


I could have easily edited those mistakes from my YouTube video, but I decided to leave them. I hope they serve as a good showcase of the teaching power of Test-Driven Development.

You don't have to be a genius to write great software. You only need to discover your mistakes fast enough to learn from them and iterate.

Want more of these posts?

Subscribe to receive new posts in your inbox.