Monday, November 5, 2007

Test Driven Development

Test-driven development (TDD) is a particularly useful technique to grow or evolve clean, refactored and readable code with a high degree of confidence. Test driven development is oftentimes confused with a code project that has unit tests, these approaches are very different, and so are the outcomes.

Why would I tell you this?
Well, since using TDD, I’m hugely impressed with the outcome, and the quality of code that can be produced. I talk to people about the technique and sometimes get people to give it a try, but often that attempt only gets as far as unit testing, or don’t perform the process properly and they see neither benefit or point to it at all. I hope to change that view for you.

Where do I start?
To start test driven development, you need tests to drive the development! Those tests are likely to have come from business requirements, so if you don’t have those, stop, and get them; otherwise you have no hope of completing this exercise at all.

The requirements are usually expressed in terms of what something has to do and so can be quite easily mapped to tests. Now at this stage, it is quite likely that the tests, or manifested requirements are expressed about the system as a whole. That’s okay, but we need to know what that means – it means initially we’re building an integration test, something that tests the system as a whole, the sum of its parts.

When using TDD, there is a common mantra, “Red, Green, Refactor”, which are a basic description of what has to happen following the writing of one test. All will become clear as you read this article.

Now, let us assume that we have a simple problem, which we want to convert numbers to words to be printed on a cheque, or money order. So for example if the amount were $599.53 then our output would be “FIVE HUNDRED AND NINETY NINE DOLLARS AND FIFTY THREE CENTS”, our constraint will limit the amount to one thousand dollars.

We can start with something simple to convert $10 to words.

To create our first test, we need to add a class library for our tests, and add references to the NUnit framework. I will not be going into how NUnit is used here, but will supply a link to some NUnit information pages.

Here is the first test:


using NUnit.Framework;

using System;

using WiseLittleBirdie.ChequeWriter;

namespace WiseLittleBirdie.ChequeWriter
{
[TestFixture]
public class ChequeTests
{
[Test]
public void CanConvertTenDollarsTest()
{
ChequeWriter chequeWriter = new ChequeWriter();

Double chequeAmount = 10.00;

string expectation = "TEN DOLLARS AND ZERO CENTS";

string result = chequeWriter.Convert(chequeAmount);

Assert.AreEqual(expectation, result, "The conversion of 10.00 dollars did not return the correct result.");
}
}
}

Notice that the test so far tests for the assumption that the chequeWriter can be created, and that we’re then testing for the output to be what we expect it to be? We will come back to that issue soon.

The code we’re now going to write, will allow the code to compile, but the test to fail due to an incorrect result.

using System;
using System.Collections.Generic;
using System.Text;

namespace WiseLittleBirdie.ChequeWriter
{
public class ChequeWriter
{
public string Convert(double amount)
{
return null;
}
}
}

Now, the code will compile, and run, and our test will fail, which checks that the test is indeed valid for at least one failure value. Due to the colour of the circle next to a failed test, this is referred to as “Red”.

Now we need to make the code pass the tests, so we implement the minimum to allow the situation to be true.

using System;
using System.Collections.Generic;
using System.Text;

namespace WiseLittleBirdie.ChequeWriter
{
public class ChequeWriter
{
public string Convert(double amount)
{
return "TEN DOLLARS AND ZERO CENTS";
}
}
}

Now we see the test will pass, referred to as “Green”. We’re happy now, aren’t we? Well, yeah, but that code really isn’t very useful yet, is it?

Now onto the final phase, refactor. We need to look at the test code, and see if we can make that neat, tidy and economical.

First of all, since we’re going to be continually changing code, there are some things we need to consider:

· Should we refer to objects using interfaces? Refactoring the interface and code isolates us slightly from the changes that we will make.
· Should we find a way to define how we create our objects, so that if we were to change our constructor, it won’t immediately mean global search and replace?

Do we need to implement either of these techniques yet? The answer to this question should probably be no. As yet, we have no requirement to do so, since there’s only one object and only one test. Arguably though, if you know that you’re going to have to do it, it may decrease the pain if the structure is implemented in the beginning.

So at the moment, we have no worthwhile refactoring to do.

Let’s expand the tests to a further case, a similar one to allow us to show some progress.

We will test now for $999.99 now, and compare the result to what we expect again.

[Test]
public void CanConvertNineHungredNinetyNineDollarsTest()
{
ChequeWriter chequeWriter = new ChequeWriter();

Double chequeAmount = 999.99;

string expectation = "NINE HUNDRED AND NINETY NINE DOLLARS
AND NINETY NINE CENTS";

string result = chequeWriter.Convert(chequeAmount);

Assert.AreEqual(expectation, result, "The conversion of 10.00 dollars did not return the correct result.");
}

Now, since we have already written a convert function for ten dollars, we would expect it now to fail. We compile the code, and run the test, and it does indeed fail, as expected.

We now need to make the test pass, again, doing the minimum we require to make that happen. We don’t want to implement anything more than we need to, so that we don’t end up with untested code, or code that simply isn’t required.

Here is the newly changed Convert Method.

public string Convert(double amount)
{
if (amount == 10)
{
return "TEN DOLLARS AND ZERO CENTS";
}
else
{
return "NINE HUNDRED AND NINETY NINE DOLLARS AND NINETY
NINE CENTS";
}
}

Now, can we refactor our code yet? Certainly! Our test classes have a lot of repetition, so let’s refactor. Here is the new test code.


using NUnit.Framework;

using System;

using WiseLittleBirdie.ChequeWriter;

namespace WiseLittleBirdie.ChequeWriter
{
[TestFixture]
public class ChequeTests
{
private void TestConversion(double amount, string expected)
{ ChequeWriter chequeWriter = new ChequeWriter();

Double chequeAmount = amount;

string expectation = expected;

string result = chequeWriter.Convert(chequeAmount);

Assert.AreEqual(expectation, result, "The conversion
did not return the correct result.");
}

[Test]
public void CanConvertTenDollarsTest()
{
TestConversion(10, "TEN DOLLARS AND ZERO CENTS");
}

[Test]
public void CanConvertNineHungredNinetyNineDollarsTest()
{
TestConversion(999.99, "NINE HUNDRED AND NINETY NINE
DOLLARS AND NINETY NINE CENTS");
}
}
}


It now looks a lot neater, and it much more readable. If we run the tests again, we can see that we still get a green light, which mean refactoring did not change the result of our code up until this point.
I left the refactoring of the CheckWriter object to include an Interface and creation in a factory still because that call only happens in one place. It’s also possible to put the creation of the ChequeWriter object into a Setup method for the test class, which would be equivalent to what you can see above. I decided to do it this way at this time because then it makes the code readable.

I know that you’re thinking that this is a pretty stupid program, and I’d have to agree, at this stage it is. One thing we can say about our development effort is that without debugging, we can tell whether a value of $10 or $999.99 can be converted readily to be printed on our cheque. We can be 100% sure that this is the case.


Now, we need to come up with a further case. We now want to convert $109.99 into words.

[Test]
public void CanConvertOneHundredAndNineDollarsTest()
{
TestConversion(109.99, "ONE HUNDRED AND NINE DOLLARS AND
NINETY NINE CENTS");
}

That was nice and easy, wasn’t it? So there is a real benefit already.

Now onto the red phase.
public string Convert(double amount)
{
if (amount == 10)
{
return "TEN DOLLARS AND ZERO CENTS";
}
else if (amount == 999.99)
{
return "NINE HUNDRED AND NINETY NINE DOLLARS AND NINETY
NINE CENTS";
}
else
{
return null;
}
}

It fails, as we’d expect it to.

And now green.

public string Convert(double amount)
{
if (amount == 10)
{
return "TEN DOLLARS AND ZERO CENTS";
}
else if (amount == 999.99)
{
return "NINE HUNDRED AND NINETY NINE DOLLARS AND NINETY
NINE CENTS";
}
else
{
return "ONE HUNDRED AND NINE DOLLARS AND NINETY NINE
CENTS";
}
}

And that passes.

Now refactoring. It seems that since all our tests have been the same so far, we have no cause to refactor any further.

If we now do test driven development in the same way with more numbers we should expect that our code will grow. We should expect that the more test cases that we have, the more the code will become useful for other cases without the need for extra code. The situation is possible largely because of the refactoring phase which is about writing more economical code. The will be a point at which all the cases will be more efficiently expressed using a method or two to calculate the words dynamically. I won’t go any further at this point so that we don’t lose focus on the technique, but suffice to say that we should be able to implement a useful piece of code if we continued.

After we’ve written code to ultimately pass each test, and refactored, our solution should converge on a result that implements all cases. If it doesn’t then we need more tests.

It is important when writing code using test driven development, that we do not miss out stages. Incorrectly writing a bunch of tests then making them pass will miss out on the important stages of testing the tests, and refactoring. If instead we wrote a test, then wrote more code than was necessary to pass the test, we would also end up with an implementation that bigger than required (code bloat). This increase in code size to implement more that can be tested by one of our tests could allow untested code to creep into our project. We don’t want that as that is uncertainty.

So far we have been developing our tests as a black box, such that we have no idea of the implementation of the class. If we were working on a more complex problem, we would then start to define a set of white box tests to logically test the workings of the dependent classes. It is important with these unit tests, to only test the class which you are focussed upon. Any dependencies should be able to be passed into the class constructor, so that it can be replaced with a mock object. Please be on the look out for new articles based on dependency injection and mock objects.

Earlier I mentioned about making an assumption about being able to create a CheckWriter object. In the unit tests above, we would and should test things such as whether we can create the object. Other things to look for are testing somewhere if we can create a factory object should we require one. Factory objects will also be discussed in a future article.

An example of something that might require white box testing could be a data layer that is called by our top level class, to save some aspect of the data that we are manipulating. This class would be written again using TDD, but the tests be at more of a logical level. This would involve the tests not being based on instance data, and the correct operation of other classes, but only on them doing what is expected under the many circumstances that are defined by the unit tests.

One very useful type of tool for test driven development, in fact for unit testing in general, is a code coverage tool. The idea here is to make sure that there are tests sufficient enough to exercise all code execution paths within the class. A reasonable aim would be to get 80% coverage at least, as it is quite a challenge to get that sort of coverage. Users of the tool should not be tempted to fake calls by making them raise dummy code just to make the coverage value go up. You must always make the tests force execution of a given path, thereby testing the real code, and hence reducing the uncertainty that would be otherwise present.


NUnit: http://www.nunit.org/
Coverage Eye.NET: http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=881a36c6-6f45-4485-a94e-060130687151

No comments: