An introduction to Test-Driven Development
02 Nov 2007Last week, I gave a short talk on Test-Driven Development, in C#, using NUnit, at the Silicon Valley Code Camp 2007. I was initially planning on posting the slides I presented, but it seemed to me that the dynamics of the process were getting lost, so I opted for a step-by-step tutorial, following essentially the code I wrote during the session.
I will follow-up shortly with another tutorial centered specifically on setting up and using NUnit, and then a few more on slightly more advanced questions. This first installement is aimed at C# developers who are not familiar with Test-Driven Development - and my objective is to provide you with the essential elements you need to get started, in an hour or so!
In a nutshell, and in the words of Kent Beck in “Test-Driven Development By Example” (Addison-Wesley), here are the steps of Test-Driven Development:
Red - Write a little test that doesn’t work, and perhaps doesn’t even compile at first.
Green - Make the test work quickly, committing whatever sins necessary in the process.
Refactor - Eliminate all of the duplication created in merely getting the test to work.
Step 0: starting point
Our mission will be to add a new functionality to an existing system, the “ShippingSystem”. The additional feature we need to put in place is the computation of the distance between cities that belong to the system. For the sake of the demonstration, we will take some liberties with reality, and conveniently assume that the world is flat, and that the distance between two points is simply:
distance(A,B) = sqrt((xA - xB)^2 + (yA - yB)^2)
To keep things simple, the “ShippingSystem” will be represented by a totally stripped-down version, namely a single class City.cs, with no members, methods or constructors, sitting all by itself in the “ShippingSystem” folder of project “IntroToTDD”. Of course, a real system would contain a gazillion of classes performing all sorts of fascinating things - but these other features are totally irrelevant to our point, and would just add confusion, so I will let your imagination fill in the gaps.
Step 1: set up NUnit
After installing NUnit on your development machine, add a reference to the NUnit.Framework to the project. In the solution explorer, right-click on “References”, select “Add Reference”, and in the .NET tab, select “NUnit.Framework”.
Add a new public class “TestsCity” to the folder ShippingSystem. This class will contain all the tests pertaining to the behavior of the class “City”. The class needs to be public in order to be visible from the NUnit GUI. Add “using NUnit.Framework” in the “using…” list, and the attribute [TestFixture] right before the line “public class TestsCity”. [TestFixture] declares that this class is a test fixture (duh!); tests that are added to that class will be visible through the NUnit GUI.
using System;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
namespace IntroToTDD.ShippingSystem
{
[TestFixture]
public class TestsCity
{
}
}
To verify that the setup was succesful, let’s check that NUnit can now “see” the test fixture. Rebuild your project (select Build > Batch Build > Select All > Rebuild), and open the IntroToTDD project with NUnit. Launch the NUnit GUI (from the Windows start menu), select Open Project, and navigate in the folder where your solution is stored. Go to IntroToTDD/IntroToTDD/IntroToTDD/bin/ , and select IntroToTDD.dll.
In the NUnit window, you should now see a tree view, with 4 levels of depth. Under IntroToTDD, there is a node ShippingSystem, containing another node “TestsCity”. NUnit can see that class because it is marked as a test fixture. Select the top node in the tree view, and press “Run”. All nodes should be marked with a ?, and on the right-hand side you should see in bold “Test Cases: 0 Tests Run: 0 “… This indicates that so far, there is no test to run. Let’s change that right now!
Step 2: write the first test
The first step in the process will be to write a test; fair enough, but what test should we write? Red - Write a little test that doesn’t work, and perhaps doesn’t even compile at first. The first idea which comes to mind would be to immediately test for the distance between 2 cities. Such a test would probably look something like “If City A has these coordinates, and City B has those coordinates, then the distance should be …”. We could start there, but that seems like a big first step, involving multiple functionalities which are not in place yet. So let’s take a mental note - or, much better, an actual note on a to-do list - and look for some easier step which would get us closer to the goal of having a running test.
One step in that direction would be to take a piece of the previous test, and focus on the statement “If City A has these coordinates…”. To compute the distance, we need City coordinates - how do we want to interact with these? To verify the coordinates of City A, we need to set them, and then check them. Let’s make this a test.
A test here is nothing more than a public method, written in a Test Fixture, prefaced by the attribute “Test”. Without worrying about whether it builds or works, let’s write a method “TestSetAndGetCityCoordinates”, which does just what we said:
[TestFixture]
public class TestsCity
{
[Test]
public void TestSetAndGetCityCoordinates( )
{
City sanFrancisco = new City( );
sanFrancisco.SetCoordinates( 3.0, 4.0 );
Assert.AreEqual( 3.0, sanFrancisco.XCoordinate );
Assert.AreEqual( 4.0, sanFrancisco.YCoordinate );
}
}
The test should be pretty self-explanatory: what we want to verify is that if we create a “City” sanFrancisco and set its coordinates to 3 and 4, we should be able to verify that the coordinates are now 3 and 4. “Assert.AreEqual” is the method provided by NUnit to validate that the system is in the state you expect it to be. Asserts come in multiple flavors; in this case, it will verify that the value you expect to see returned (which you have to explicity provide) is equal to the value that is actually returned by the system.
Step 3: get it to build
At that point, our code does not compile, because City knows nothing yet of SetCoordinates, XCoordinates and YCoordinates; we need to address that first. Let’s right click on the SetCoordinates() method, and select “Generate Method stub”, to automatically add it to “City”. Unfortunately, we can’t use that trick for properties, so let’s go to City and try to get to compile by changing as little as possible. Let’s add 2 read-only, internal properties returning 0.0, and let’s also remove the exception thrown in SetCoordinates().
public class City
{
internal double XCoordinate
{
get
{
return 0.0;
}
}
internal double YCoordinate
{
get
{
return 0.0;
}
}
internal void SetCoordinates( double x, double y )
{
}
}
Now let’s
rebuild - success! We can now go to NUnit, and select “Reload project”. Under
the node “TestsCity”, we have now one new item, “TestSetAndGetCityCoordinates”,
which is the test we just wrote. Run the test: all nodes show up red, and on the
right a message says "IntroToTDD.ShippingSystem.TestsCity.TestSetAndGetCityCoordinates : Expected: 3.0d, But was: 0.0d"
.
The red signals that the test fails
(no surprise), and the reason is that while the XCoordinate of San Francisco was
expected to be 3.0, it was actually 0.0. Given that XCoordinate returns 0.0 no
matter what, this does not come as a surprise; we know what we need to do
next.
Step 4: get it to green
Now that we have a red test, we need to get to green - Our objective is to “Make the test work quickly, committing whatever sins necessary in the process”. There is a very small modification which would get us there: replace the hardcoded 0.0 returned by the properties, by the values the test expects. No doubt, it’s a sin, but the rules say we are allowed to do it, so let’s go for it:
internal double XCoordinate
{
get
{
return 3.0;
}
}
internal double YCoordinate
{
get
{
return 4.0;
}
}
Rebuild, run the test: the nodes in the NUnit tree view are now green. We are happy.
Step 5: refactor to remove duplication
At that point, you might feel a bit uneasy; the way we made the test pass is absolutely horrendous. That’s why there is a third step: “Eliminate all of the duplication created in merely getting the test to work”. Duplication usually refers to “code duplication”: the same functionality is performed in two (or more…) different places in the application, by equivalent code. There is no obvious duplication of that type here. On the other hand, we have “data duplication”: the same piece of data is hardcoded in two different places: in the code, and in the test that verifies it.
To remove the duplication, we simply need to change the code, so that instead of a hardcoded value, it operates on a variable. Let’s add two members m_XCoordinate, and m_YCoordinate, to City, and redirect the properties XCoordinate and YCoordinate to return the variable instead of a constant.
public class City
{
private double m_XCoordinate;
private double m_YCoordinate;
internal double XCoordinate
{
get
{
return m_XCoordinate;
}
}
internal double YCoordinate
{
get
{
return m_YCoordinate;
}
}
internal void SetCoordinates( double x, double y )
{
}
}
We rebuild and run the test: we are back to red. Damn.
Step 6: get back to green
Why is the test back to red? NUnit tells us that while a 3.0 is expected in the test, the actual result is 0.0. After a quick inspection of the code, the source of the problem is clear: we have never passed the values of SetCoordinates to the member variables. Let’s fix that.
internal void SetCoordinates( double x, double y )
{
m_XCoordinate = x;
m_YCoordinate = y;
}
Rebuild, run the test: we are green.
As an aside, note how having a test in place just makes life easier: we introduced a small bug, but it immediately came to the surface. Granted, this case was pretty trivial, but it proves the point. Having tests is place is just so reassuring: you get instant feedback the exact moment you introduce a bug.
Step 7: refactor?
Is there any duplication left? No, so we can move one and add a new test.
Step 8: add a new test
Now we can get back to our initial test idea, “If City A has these coordinates, and City B has those coordinates, then the distance should be …”. Are we ready to take that one? I think so: the issues with coordinates are resolved, and the only new element here is that we need to verify a “Distance”.
The pattern we used in the previous iteration is called by Kent Beck “Fake it” (‘Til You Make It). Write a test that fails, make the test run by returning a constant, and gradually transform the constant into an expression using variables. We could use the same approach here, but for the sake of variety (and pedagogy) we will illustrate a different pattern, “Triangulate”. In the words of Beck again, “we only generalize code when we have two or more examples”. Let’s write a test with two examples of Distance computation:
[Test]
public void TestDistanceBetweenCities( )
{
City sanFrancisco = new City( );
City losAngeles = new City( );
City paloAlto = new City( );
sanFrancisco.SetCoordinates( 0.0, 0.0 );
losAngeles.SetCoordinates( 0.0, 2.0 );
paloAlto.SetCoordinates( 3.0, 4.0 );
Assert.AreEqual( 2.0, sanFrancisco.Distance( losAngeles ) );
Assert.AreEqual( 5.0, sanFrancisco.Distance( paloAlto ) );
}
To get it to build, we generate the stub for the Distance method, and clean up the return type and the argument name. Let’s run the test: we have a red test, and the message we get from NUnit is “method not implemented”: let’s implement.
Step 9: implement!
The difference between “Triangulate” and “Fake It” is immediately apparent here. Because we have two Distance computations checked in the test, there is no way we can hardcode a return value to get to green. We need to implement it for real. Let’s start, with the end in mind: we need to return a distance:
internal double Distance( City otherCity )
{
double distance = 0.0;
return distance;
}
Rebuild, and check - everything is still red. Let’s add the computation of the distance, as the square root of the sum of the square of the differences between the coordinates (or something like that):
internal double Distance( City otherCity )
{
double deltaX = otherCity.XCoordinate - this.XCoordinate;
double deltaY = otherCity.YCoordinate - this.YCoordinate;
double distance = 0.0;
distance += deltaX * deltaX;
distance += deltaY * deltaY;
return Math.Sqrt( distance );
}
We could stop there, but because we like our code to be totally self explanatory, we will rename “distance” to “squareOfDistance”, which is a better description of what the variable stores.
internal double Distance( City otherCity )
{
double deltaX = otherCity.XCoordinate - this.XCoordinate;
double deltaY = otherCity.YCoordinate - this.YCoordinate;
double squareOfDistance = 0.0;
squareOfDistance += deltaX * deltaX;
squareOfDistance += deltaY * deltaY;
return Math.Sqrt( squareOfDistance );
}
Step 10: refactor?
There is no obvious duplication left - we are done! You could still change a thing here or there, but at that point this would be mostly a matter of taste.
Conclusion
I came across Test-Driven Development through the book of Kent Beck a while back, and in all honesty I think it has been the most influential book in my practice of software engineering. What I tried to re-create here are the two things I liked most about it: its focus on simple and very actionable steps, which any software engineer can use, and the way the book itself is written; it literally showed code being written, page after page, with “live” commentary, and made it very easy to see how it was supposed to work when actually being used.
At the end of the presentation, someone asked me if I really used it. I definitely do, but I adapt the rhythm, depending on the circumstances. When writing very easy code, I would maybe skip a few steps and speed up the process; when I hit a difficult problem, I slow down and go back to very systematic red/green/refactor. It’s really a question of common sense, and of finding what works for you. There is one step I won’t skip, though - and that is writing tests as a go, hand in hand with writing code.