TDD by Example: How to add tests against code that doesn’t exist yet

I personally use Test-Driven Development (TDD) on a daily basis because it allows me to focus on solving one test case at a time in the simplest way possible, and then refactor with the safety net of unit tests. It’s a bottom-up approach, and is a natural fit for my coding principles (unit test, refactor, be pragmatic).

With TDD you add tests first, then add code to make the tests pass. This test-first approach confused me when I first learned about TDD. It’s counter-intuitive. How can you possibly add a test against code that doesn’t exist yet?

Then another developer explained it to me. You add the test as if the code existed, then you stub out the missing code so it compiles, then you run the test and verify it fails.

If you add the “stub out the missing code under test step” to the TDD process steps, it really clears things up.

TDD Process: For each test case 1. Add a test 2. Stub out missing code under test (if necessary) 3. Verify the test fails 4. Write code 5. Verify all tests pass 6. Refactor (if necessary)

In this article I’ll show an example of this test-first approach by solving the well-known FizzBuzz problem.

1 – FizzBuzz requirements as when-then statements

The FizzBuzz problem is a well-known interview screening question.

The requirements are simple, you’re given an integer N:

  • When N is divisible by 3, return “fizz”
  • When N is divisible by 5, return “buzz”
  • When N is divisible by 15, return “fizz buzz”
  • When N is not divisible by 3, 5, or 15, it returns “N” (ex: 2 would return “2”)

2 – Add first test case against the non-existent FizzBuzz code

With TDD we only need to focus on one test case at a time. So we’ll start with: When N is divisible by 3, return “fizz”.

TDD step 1 – Add a test

I added a test case against the FizzBuzzer class using the GetFizzBuzz() method. This class and method don’t exist yet.

Unit test against FizzBuzzer class. This class doesn't exist yet, so the compiler is showing a red squiggly line.

As you can see, the compiler is mad and showing red squiggly lines because the class doesn’t exist.

TDD step 2 – Stub out the missing code under test (if necessary)

Create the FizzBuzzer class and add the GetFizzBuzz() method.

You can either do this manually or use your IDE to generate the type and method. In my case I’m using Visual Studio.

Here’s how to generate the new type and method in Visual Studio.

  • Right-click the FizzBuzzer class, then Ctrl + .
  • This will bring up a refactoring list, click Generate new type…
  • Point to your project and create a new file with the class name, then click OK.
Generate New Type modal in Visual Studio
  • Now the GetFizzBuzz() method is going to show a red squiggly.
  • Right-click on GetFizzBuzz(), then Ctrl + .
  • Then click the only option available Generate method…
  • This will generate a stubbed method in FizzBuzzer called GetFizzBuzz() that throws NotImplementedException.
  • Take a look at the generated method.
public object GetFizzBuzz(int n) { throw new NotImplementedException(); }
  • Notice that the return type is object. Change this to string.
public string GetFizzBuzz(int n) { throw new NotImplementedException(); }

Now the compiler is happy and I can run the test.

TDD step 3 – Verify the test fails

Run the test and make sure it fails.

TDD - verify the test fails

Always make sure the test fails at first, otherwise you can’t prove that your code is what’s making it pass.

This is the same problem with adding unit tests after you’ve written the code: you aren’t really proving that your code works. It could be a false positive (passed when it should’ve failed). This is the power of the test-first approach: you are proving that your code works.

TDD step 4 – Write code

Next, we need to solve the test case. If n is divisible by 3, return “fizz”.

public string GetFizzBuzz(int n) { if (n % 3 == 0) { return "fizz"; } return ""; }

TDD step 5 – Verify all tests pass

Run all of the tests. In this case, we only have the one test. Sure enough, it’s passing the test case.

TDD - verify all test cases are passing

TDD step 6 – Refactor (if necessary)

The code is so simple at this point that there’s nothing to refactor. We can move on to the next test case.

3 – Repeat TDD process for test case – when N is divisible by 5, return “buzz”

TDD step 1 – Add a test

[TestMethod()] public void WhenN_IsDivisibleBy5_ReturnsBuzz() { //arrange FizzBuzzer fizzBuzzer = new FizzBuzzer(); int n = 5; var expected = "buzz"; //act var actual = fizzBuzzer.GetFizzBuzz(n); //assert Assert.AreEqual(expected, actual); }

TDD step 2 – Stub out the missing code under test (if necessary)

Not necessary here. Already have the FizzBuzzer class and GetFizzBuzz() method.

TDD step 3 – Verify the test fails

Yup, it fails.

TDD - verify test fails

TDD step 4 – Write code

If n is divisible by 5, return “buzz”.

public string GetFizzBuzz(int n) { if (n % 3 == 0) { return "fizz"; } else if (n % 5 == 0) { return "buzz"; } return ""; }

TDD step 5 – Verify all tests pass

Now that there are multiple test cases, it’s important to make sure they all pass after your code change.

This is a great benefit of TDD: short feedback loops. You can catch bugs right away and know for sure that it was caused by the code change you just did.

TDD step 6 – Refactor (if necessary)

Notice that the two unit tests are almost exactly the same. The only difference is the input and expected output.

This means we can refactor them by creating a parameterized unit test.

Before refactoring the unit tests

[TestMethod()] public void WhenN_IsDivisibleBy3_ReturnsFizz() { //arrange FizzBuzzer fizzBuzzer = new FizzBuzzer(); int n = 3; var expected = "fizz"; //act var actual = fizzBuzzer.GetFizzBuzz(n); //assert Assert.AreEqual(expected, actual); } [TestMethod()] public void WhenN_IsDivisibleBy5_ReturnsBuzz() { //arrange FizzBuzzer fizzBuzzer = new FizzBuzzer(); int n = 5; var expected = "buzz"; //act var actual = fizzBuzzer.GetFizzBuzz(n); //assert Assert.AreEqual(expected, actual); }

After refactoring the unit tests

To refactor:

  • Add a parameterized unit test.
  • Add the two test cases: when divisible by 3, then “fizz” and when divisible by 5, then “buzz”.
  • Remove the other unit tests.
[DataRow(3, "fizz")] [DataRow(5, "buzz")] [TestMethod()] public void WhenN_ReturnsExpected(int n, string expected) { //arrange FizzBuzzer fizzBuzzer = new FizzBuzzer(); //act var actual = fizzBuzzer.GetFizzBuzz(n); //assert Assert.AreEqual(expected, actual); }

Verify the tests still pass after refactoring

Since we did the refactoring step, we need to re-run all the tests and verify they all pass.

TDD - verifying parameterized unit test passes after refactoring

4 – Repeat TDD for test case – when N is divisible by 15, return “fizz buzz”

This is the fun part about the FizzBuzz problem.

If someone is rushing to solve the problem without actually verifying it works, they will often make the following mistake of checking the “fizz buzz” case last:

if divisible by 3, return "fizz" else if divisible by 5, return "buzz" else if divisible by 15, return "fizz buzz"

Because 15 is divisible by 3, this will return “fizz” instead of “fizz buzz”.

With a test-first approach, it’s very hard to make this mistake, because your test case will fail.

This is why it’s an interesting interview question: details matter, verifying correctness matters.

TDD step 1 – Add a test

If N is divisible by 15, return “fizz buzz”. We can simply add a new DataRow to the existing parameterized test.

[DataRow(3, "fizz")] [DataRow(5, "buzz")] [DataRow(15, "fizz buzz")] [TestMethod()] public void WhenN_ReturnsExpected(int n, string expected) { //arrange FizzBuzzer fizzBuzzer = new FizzBuzzer(); //act var actual = fizzBuzzer.GetFizzBuzz(n); //assert Assert.AreEqual(expected, actual); }

TDD step 2 – Stub out the missing code under test (if necessary)

Nothing to do here, all the code exists.

TDD step 3 – Verify the test fails

It fails as expected.

TDD step 4 – Write code

Notice why the test case fails – it’s returning “fizz”. This is because 15 is divisible by 3. It’s also divisible by 5. This tell us we need to add the “divisible by 15” check first.

public string GetFizzBuzz(int n) { if( n % 15 == 0) { return "fizz buzz"; } else if (n % 3 == 0) { return "fizz"; } else if (n % 5 == 0) { return "buzz"; } return ""; }

TDD step 5 – Verify all tests pass

They all pass.

TDD step 6 – Refactor (if necessary)

Nothing to refactor.

5 – Repeat TDD process for the final test case – When N is not divisible by 3, 5, or 15, it returns “N”

TDD step 1 – Add a test

Since we are using a parameterized test, we can easily add new test cases. So let’s add a few test cases here.

[DataRow(3, "fizz")] [DataRow(5, "buzz")] [DataRow(15, "fizz buzz")] [DataRow(1, "1")] [DataRow(2, "2")] [DataRow(4, "4")] [DataRow(7, "7")] [TestMethod()] public void WhenN_ReturnsExpected(int n, string expected) { //arrange FizzBuzzer fizzBuzzer = new FizzBuzzer(); //act var actual = fizzBuzzer.GetFizzBuzz(n); //assert Assert.AreEqual(expected, actual); }

TDD step 2 – Stub out the missing code under test (if necessary)

Nothing to do here.

TDD step 3 – Verify the test fails

Yup, it fails.

TDD - when N not divisible by 15, 5, or 3, it returns "N". Verify test case fails.

TDD step 4 – Write code

When it’s not divisible by 15, 3, or 5, return n as a string.

public string GetFizzBuzz(int n) { if( n % 15 == 0) { return "fizz buzz"; } else if (n % 3 == 0) { return "fizz"; } else if (n % 5 == 0) { return "buzz"; } return n.ToString(); }

TDD step 5 – Verify all tests pass

All green!

TDD - verify all test cases pass after fully implementing FizzBuzz

TDD step 6 – Refactor (if necessary)

There’s nothing to refactor, it’s as simple as possible.

Leave a Comment