Test driven development seems to divide programmers into three different camps; those that think that it is the best thing since sliced bread, those that think it is a good idea but impractical due to the time it takes to use, and those that are completely ignorant of what exactly it is.
It seems, that if you start to try using TDD for any projects, it will very quickly become clear why you might want to implement using this technique. Simply, it isn't so much about writing tests, but more about evolving a solid, loosely coupled solution that meets the needs of the requirements and nothing more. From the point of view of that statement, surely nobody would argue that up front design is a good idea, and in so agreeing to that statement, you also agree that you agree with the principles of TDD.
But there's more! As a by-product of evolving this wonderful software design, that is neither over-engineered nor highly coupled, we also end up with tests, and code!
It probably seems strange that I have mentioned that we end up with tests; very quickly a practitioner of TDD will notice that tests are manifestations of the requirements.
So we're really looking at test driven development from the point of view of what we want to do with the solution, which is the software's requirements. This technique doesn't in any way imply that we don't gather requirements, but what is does do is allow us a framework by which to evolve yet more requirements as they become apparent. What I mean by that is that we have a bunch of tests, that will always test for functionality, and should that functionality cease to work correctly, the test will fail, and the requirement will not be met. This means that we are then able to add, or change functionality, and know that a particular requirement is or is not met.
Well, I've worked on a few projects in my time, and in every one, we are asked how far from completion we are. How can we tell? Well, if we're sensible, we have some unit tests. The number of not yet implemented tests, and the number of failing tests should give us an idea of how far we are away from a successful build of the solution. We now have a technique that allows us to design our software according to requirement, and produces tests to give us an idea of how we are progressing.
Upon implementing the code to allow a particular test to pass, we also develop our code solution. Now, I'm wondering how we could possibly not want to develop good mature designs, unit tests and a solution using a technique that is directly aimed at that process?
One major barrier to the use of TDD are things like patterns. Don't misquote me here, design patterns are awesome, but what they often do is give a developer a design possibly before they fully understand the problem space. What that means is that as a developer, we're often tempted to jump straight in and start coding, without full consideration of the requirements. TDD forces you to implement only what is needed of the solution and nothing more. Design patterns lead developers to thinking that TDD is slow because they already know all the answers. What they should really be asking is, does that design pattern come with code and unit tests already constructed for their requirements. Almost certainly it does not.
Instead of rushing to start with a preconceived idea of implementation, a developer should instead use TDD to evolve the design. When it becomes apparent that the solution has evolved to a point at which the use of a design pattern is appropriate, to solve a requirement, then it should be used. In using this technique, a developer has documented evidence that such a pattern is a requirement of the solution, through the tests that are already in place.
Test driven development really requires the use of mock objects, which are strange to say the least, until you understand how they work, and then they become invaluable. When the design pattern requirement becomes apparent, a new set of tests should probably be constructed to test the pattern separately. In the original test project where the use of a pattern was discovered, mock objects should be used to 'pretend' that the pattern is being called.
We use mock objects so that a particular test class only tests one class directly, and everything else is mocked, and so has been assumed to be working unless the mocked code's unit tests fail.
A big part of TDD is that we end up with a very loosely coupled solution. This is largely due to the mock objects that I previously mentioned. Mock objects should really use interfaces to access code, and not concrete classes. Using a class mock would make the implementation be coupled to that class and so would be easily broken, or brittle in construction.
In working through code attempting to use interfaces, a developer will often come across resistance in one way or another. The problems may be due to the fact that the .NET framework does not offer an interface to a particular object directly. In that instance, what that is actually telling you is that you need to create your own wrapper class and interface. This is actually good news, if you stop and think about it, you are in effect abstracting functionality away from the implementation in the framework. This process would allow refactoring to take place in a much simpler way, and would also allow that implementation to be swapped out should the there be the need to do so. It is starting to sound like the beginnings of a Strategy, or Provider pattern now isn't it?! Shhh! Don't tell the naysayers!
One thing of note is that when the developer is trying to mock and test a particular aspect of the system, and it becomes hard work, that is often a good point at which to reconsider the implementation of those requirements. In the case of an object that doesn't have an interface, that leads us to wrapping up something that essentially provides functionality for us. Implementing tests on such a design should be once again very straight forward.
Generics are somewhat of an exception to the rule. They're great for giving type to classes, but unless you can perform all your tests on the functionality with only interfaces, you should consider refactoring the design, or hiding the generic interfaces behind non-generic interfaces. If this becomes too hard still, you may well find yourself wondering whether the generic class is really appropriate. This process is good, but don't do as many people do and blame TDD because you can't test the code; or at least don't keep the generic class that you can't test unless you're happy to have all aspects of the system that the generic class touches also be untested.
Red - green - refactor. What a shame that somebody came up with that as a mantra for TDD. Write the test, it fails, implement the code to make it pass. Seems pretty straight forward in concept, but as you can see from the discussion above, there is so much more to it.
It is strange that so many people like the idea of TDD, but so few have actually found a use for it. It is also a shame that there is so little information on the use of TDD and its practical implementations. I have seen many a simplistic example, and have given then myself in times, but when the technique is used commercially we want to be able to use it to its full potential, and solve every problem we come across with it, otherwise we simply will not use it.
If anybody has questions regarding TDD, please leave comments, and I will do my best to find answers, or offer possible solutions for you.
Saturday, December 29, 2007
Real world test driven development (TDD) - Perhaps "Requirements Driven Design and Development"?
Posted by
Martin Platt
at
4:35 PM
0
comments
Labels: real, TDD, Test Driven Development, Unit Test, world
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
Posted by
Martin Platt
at
4:25 PM
0
comments
Labels: Code Coverage, NUNit, Refactor, TDD, Test Driven Development