Iím hooked on test-driven development (TDD)

Published on Aug 6, 2013 by Jamie Munro

Iíve only been doing TDD for a few weeks, but Iím completely sold.† I donít want to go back!† Iíll be honest though, it hasnít been easy.† Iíve made mistakes, Iíve wasted time, but Iím really starting to reap the benefits.

Iíve always thought I was a good developer.† I write decent code and it works mostly as expected.† It took me many years into my career before I wrote my first unit test.† It always fell into the category of too time consuming or expensive.† Oh the irony!

As I started learning how to write to unit tests, I always found myself rewriting things I already did just to get them to be unit tested; how frustrating!† A unit test that should have only took a few minutes, ended up taking a really long time because the code had to be refactored just to be tested.† No better way to turn you off from unit testing.

Enter test-driven developmentÖ




TDD is a discipline.† It requires much smaller steps than the average person is used too.† It also requires you to break rules just to get a ďgreen barĒ.† Without persistence and the ability to see the bigger picture, these rules can easily make anyone give up.

When I first started I immediately realized my pace of ďoutputĒ got slower in my mind.† It felt slow because 20 Ė 30 lines of unit testing setup might equal less than 10 lines of ďtangibleĒ code.† This is a hard pill to swallow as you start.

What would take me 15 minutes is now taking me 30 minutes.† However, the end result is incredible.† My code is so much cleaner and much more accurate.† Itís important to not focus on the small increases in time at the beginning, trust me it all washes out in the end.

The best analogy Iíve heard is the tortoise and the hare.† I used to be the hare, racing through the logic and lines of code.† Only to hit Ė what I thought was the finish line Ė to find out that I had to spend time debugging issues that arouse along the way (that I didnít catch).† Unfortunately, this is where I (the hare) began to get tired and slow down.† Issues became harder to detect.† I found them, but the cost was high as my energy got lower.

Meanwhile, the tortoise (me while doing TDD) is moving along at a nice pace addressing much simpler, smaller pieces of logic to eventually surpass the hare and win the race (or maybe tie).

Enough of the analogies, I think itís time to Ė you guessed it Ė test-drive this article with some examples.† Iíll start with the first example that I ever TDDíed Ė the Fizz Buzz example.† This coding test was first discussed by Jeff Atwood to help determine who is a good developer.

Fizz Buzz works as follows.† It receives a number as input and it outputs one of the following 4 things:

  • If the number is divisible by three, the function should output Fizz.

  • If the number is divisible by five, the function should output Buzz.

  • If the number is divisible by both three AND five, the function should output Fizz Buzz.

  • Otherwise, the number entered is simply returned as output.


I am going to write this in C#.† Iím going to write this in a new Unit Test Project.† For simplicities sake, I will write both the tests and the resulting function in the same class.

Now before I begin, there are a few different options of how this could be TDDíed:

  • One test method per expectation.

  • One test method with multiple asserts.


To me both are valid and the one I choose depends on the complexity of the functionality.† Because this is a blog article, it might work better with multiple small test methods instead of a constantly growing one.

Letís begin.† Here is the first test method:


[TestMethod]
public void Given1Expect1()
{
     var expected = 1;
     var actual = FizzBuzz(1);
     Assert.AreEqual(expected, actual);
}


As you will notice, the code currently doesnít compile because the FizzBuzz function doesnít exist.† So the first rule of TDD is to get the code to compile and a green bar as fast as possible.† Here is the simplest solution I can find:

private object FizzBuzz(int p)
{
     return 1;
}

There are a few interesting things to point out here.† Because I wrote my tests using var and I used my IDE to automatically create the function for me (because it didnít exist) it detected the correct input type as int but wasnít sure what the output type should be, so it made it an object.† Ironically this isnít a bad choice as we will see shortly.

The next thing to notice is I simply returned a constant value of 1.† If I were coding this normally I wouldnít have done it, I wouldíve returned p Ė the input parameter.† I wanted to show the incremental steps.

There are two possible next steps that I see:

  • Remove the duplication

  • Write another simple test to force us to remove the constant


Given that I see the number 1 three times in less than 10 lines of code, I think removing the duplication makes most sense.† Here is my refactoring to remove duplication of the number 1:

[TestMethod]
public void Given1Expect1()
{
     var expected = 1;
     var actual = FizzBuzz(expected);
     Assert.AreEqual(expected, actual);
}

private object FizzBuzz(int p)
{
     return p;
}

After refactoring, itís a good time to run the tests again and make sure we still have a green bar.† As expected we do.† The nice part about this refactoring is we donít need to write another test that might be called Given2Expect2, I think we can all be confident in our previous refactoring.

Here is the next obvious test I see:

[TestMethod]
public void Given3ExpectFizz()
{
     var expected = "Fizz";
     var actual = FizzBuzz(3);
     Assert.AreEqual(expected, actual);
}

As expected, this new test fails.† Time to update our function to get a green bar.† Once again I will go with the simplest solution:

private object FizzBuzz(int p)
{
     if (p == 3)
          return "Fizz";

     return p;
}

Running the tests shows a green bar.† Now we can refactor with confidence that we have our security blanket in case something goes wrong.

Because this example is pretty simple, we have the same opportunity as before.† We can write a simple test to show how the constant wonít solve the long-term problem or we can refactor the solution to not use the constant conditional to 3.

private object FizzBuzz(int p)
{
     if (p % 3 == 0)
          return "Fizz";

     return p;
}

After refactoring, itís important to run the tests and ensure we still have green.

On to the next unit test:

[TestMethod]
public void Given5ExpectBuzz()
{
     var expected = "Buzz";
     var actual = FizzBuzz(5);
     Assert.AreEqual(expected, actual);
}

Because of our previous refactoring, itís safe to skip the use of a constant and instead apply Obvious Implementation.† I often will use this rule as much as possible as I can better control the size of steps I take during my TDD efforts:

private object FizzBuzz(int p)
{
     if (p % 3 == 0)
          return "Fizz";
     if (p % 5 == 0)
          return "Buzz";

     return p;
}

Once again we have green. †At this point I donít see any obvious refactoring, so letís write another test:

[TestMethod]
public void Given15ExpectFizzBuzz()
{
     var expected = "FizzBuzz";
     var actual = FizzBuzz(15);
     Assert.AreEqual(expected, actual);
}

As expected we have a red bar:

private object FizzBuzz(int p)
{
     if (p % 3 == 0 && p % 5 == 0)
          return "FizzBuzz";
     if (p % 3 == 0)
          return "Fizz";
     if (p % 5 == 0)
          return "Buzz";

     return p;
}

Once again the bar is green.† But look at all that duplication!† 3ís and 5ís splattered all over this function.† Given that we have all green, itís safe to refactor this function.† There really are hundreds of ways to refactor this function.† The simplest one to me is leveraging one more magic number.

private object FizzBuzz(int p)
{
     if (p % 15 == 0)
          return "FizzBuzz";
     if (p % 3 == 0)
          return "Fizz";
     if (p % 5 == 0)
          return "Buzz";

     return p;
}

I see one more unit test still.† The minimum input value is 1.† What will happen if 0 is entered?† I think an exception should be thrown because itís invalid input:

[TestMethod]
[ExpectedException(typeof(IndexOutOfRangeException))]
public void Given0ExpectException()
{
     FizzBuzz(0);
}

Once again we have a red bar that we need to solve.† Knowing that anything less than 1 is invalid, letís use obvious implementation to solve the red bar:

private object FizzBuzz(int p)
{
     if (p < 1)
          throw new IndexOutOfRangeException();
     if (p % 15 == 0)
          return "FizzBuzz";
     if (p % 3 == 0)
          return "Fizz";
     if (p % 5 == 0)
          return "Buzz";

     return p;
}

I sure feel pretty confident in this code now.† More importantly, if the specifications ever changed, we have a solid foundation of tests to allow us to update with confidence.

As you can see from this very small TDD example, the steps from writing a test to writing the code feels slow and took a total of 13 steps!† The final result of the FizzBuzz function could probably be written in one giant step; however, by doing TDD we saved future us two important things:

  1. If we messed up and caused a bug, it would be harder to find in 10 lines of code versus 1 or 2 lines of code that we were adding at a time.

  2. If the requirements change, we have a safety net to work from.† Knowing how often requirements change in this industry, this is an important net to me.


Summary


Since starting TDD, Iíve made a few mistakes.† Here are the lessons Iíve learnt from them and hopefully it will help you getting started:

  1. If you are writing a function that is to be used by another function and the tests for this function feel too simple, try stopping and think whether you should test the other function instead that will use the results.
    An example of this might be if you are writing a function that returns an integer offset that will be used to calculate a date offset.† The function that returns the integer offset might be too small to test Ė unless there are important business rules happening inside this function.† Instead, test the function that calculates the date offset.† The date object that results from using the offset integer is probably more important.

  2. If you find yourself spending more time writing your setup functions for your unit testing, the class you are trying to test is probably doing too much.† This is a good opportunity to stop yourself from spending a lot of time writing mock objects and see if there is a way you can simplify the class and reduce its responsibility.

Tags: Optimization | ASP.NET | Theory | fizzbuzz | tdd | test-driven development

Related Posts

blog comments powered by Disqus