Test-Driven Development (TDD)


After having used TDD for a couple of years now, I'm still getting even fonder of the technique. I'm becoming more and more convinced that TDD is the single most important technique in becoming a better programmer. Strong words any substance? You'll have to try it out on your own to know for yourself, of course, but let's have a look at what it's all about. Let's start with a short repletion from Chapter 1, "Values to Value," about the flow.

The TDD Flow

First, you start writing a test. You make the test fail meaningfully so that you can be sure that the test is testing what you think it should.

The second step is to write the simplest possible code that makes the test pass.

The third step is to refactor.

Then you start all over again, adding another test.

If we use xUnit lingo, the first step should give you a red light/bar, and the second step should give you a green light/bar. (Unfortunately, compilation errors and refactoring don't have colors yet.)

So the mantra is Red, Green, Refactor. Red, Green, Refactor...

Time for a Demo

I'm assuming that you are familiar with Testdriven.net [Testdriven.net], NUnit [NUnit], or some other similar tool (or pick up some such tool now and learn the basics on your own). So instead of a tool discussion, I'll focus on how to apply the concept of TDD.

I know, I know. There are an enormous number of good demonstration texts on how to use TDD, such as [Beck TDD], [Astels TDD], [Martin PPP]. Anyway, I'd like to have a go at demonstrating it myself. Rather than doing what is most common, I won't use a homemade toy example, but I'll use an example from a real-world application of mine.

A few years ago, I was asked by Dynapac to write an application that they could bundle with a new line of planers that they produced. The planers are used for removing old asphalt before paving out new. Asphalt milling is used to restore the surface of asphalt pavement to a specified profile. Bumps, ruts, and other surface irregularities are removed, leaving a uniform, textured surface.

The application would be used for calculating a number of different things, helping to optimize the usage of the planers. The application needed to be able to calculate the hardness of the old asphalt, the time needed to plane a certain project, the trucks needed to keep milling with the minimum cost, and lots of other things.

I'm going to use this real-world application to demonstrate how TDD can be used so you get a feeling for the flow. I'm going to focus on calculating the number of trucks.

It is important to have the correct number of trucks for transporting the old asphalt. If you have too few, the planer will have to stop every now and then or move slower. If you have too many, it's a waste of trucks and personnel. In both cases, the cost will increase for the project.

OK, let's do this the TDD way. I'll start out by writing the identified needed functionality in a text file like this:

Calculate millability Calculate milling capacity Calculate number of trucks needed


As I said, I'm going to focus on the calculation for the number of trucks now, so I put a marker in the file on that line so I know what I decided to start with. As soon as I (or more likely the customer) come up with another piece of functionality that I don't want to work on right now, I add it to the file. So that text file will help me to not forget about anything, while still enabling me to focus on one thing at a time.

Note

Tests and refactorings could be written to the same file, but I prefer not to. Instead I write tests tagged with the Ignore attribute for tests that I don't want to focus on now. Refactoring needs that I find, but I don't want to deal with now, are probably not worth being done, or they will be found again and dealt with then.


The next thing to do is to think about tests for the truck calculation. At first I thought it was very simple, but the more I thought about it, I found out it was actually pretty complex.

So let's start out by adding a simple test. Let the test check that when no input is given, zero trucks should be needed. First, I create a new project that I call Tests. I add a class that I call TRuckCalculationTests, and I do the necessary preparations for making it a test fixture according to NUnit, such as setting a reference to nunit.framework, adding a using clause to NUnit.Framework, and decorating the class with [TestFixture]. Then I write a first test, which looks like this:

[Test] public void WillGetZeroAsResultWhenNoInputIsGiven() {     TruckCalculation tc = new TruckCalculation();     Assert.AreEqual(0, tc.NeededNumberOfTrucks); }


As a matter of fact, when writing that extremely simple test, I actually made a couple of small design decisions. First, I decided that I need a class called truckCalculation. Second, I decided that that class should have a property called NeededNumberOfTrucks.

Of course, the test project won't compile yet because we haven't written the code that it tests, so let's continue with adding the "real" project, so to speak. I add another project that I call Dynapac.PlanPave.DomainModel. In that project I add a class like this:

public class TruckCalculation {     public int NeededNumberOfTrucks     {         get {return -1;}     } }


What is a bit irksome is that I fake the return value to -1. The reason is to force a failure for my test. Remember, we should always start with an unsuccessful test, and for this specific test just returning zero would clearly not fail. It should fail meaningfully, but I can't come up with a really meaningful failure and still start out as simple (neither now nor when I wrote this in the real project), so this will have to do.

Then we go back to the Tests project and set a reference to the Dynapac.Plan-Pave.DomainModel project. We also add a using clause and build the whole solution. After that we execute the tests (or actually only the test at this point) in the Tests project. Hopefully we get a red bar. Then we go back to the property code and change it so it returns zero instead: re-execute the tests and a green bar.

Is there anything to refactor? Well, I'm pretty happy with the code so far. It's the simplest code I can think of right now that satisfies the tests (oragainthe test).

We have taken a small step in the right direction. We have started working on the new functionality for calculating number of trucks, and we have a first simplebut goodlittle test.

Let's take another small step. Again, we take the step by letting tests drive our progress. So we need to come up with another test. In the testing area, I need to show both a rounded result and a more exact result, at least to one decimal. The rounded result must always be rounded up because it's hard to create a half truck. (I know what you are thinking, but it's not in the customer requirements to deal with a mix of different-sized trucks.) That's a decent and necessary test, but I don't feel like dealing with it now. I want to take a more interesting step, so I add the rounding test with the Ignore attribute like this:

[Test, Ignore("Deal with a little later")] public void CannotRoundDecimalTruckDown() { }


Instead, I'd like to take a step with the calculation. Heck, it can't be so hard to calculate this. I have to take transportation distance into account, as well as transportation speed, truck capacity, milling capacity, unloading time, loading time, and probably a couple of other things. Moving on and simplifying a bit, let's say that I don't care about transportation for now, nor the time for loading. I won't even care about other factors that are as yet unknown to me. The only things I care about now are milling capacity, truck capacity and time for unloading. So things are simple enough for the moment. I can write a test like this:

[Test] public void CanCalculateWhenOnlyCapacityIsDealtWith() {     TruckCalculation tc = new TruckCalculation();     tc.MillingCapacity = 20;     tc.TruckCapacity = 5;     tc.UnloadingTime = 30;     Assert.AreEqual(2, tc.NeededNumberOfTrucks); }


What I just did was to assume the milling capacity was 20 tons/hour and each truck can deal with 5 tons each time, which means 4 unloadings. I also assumed that unloading the truck takes 30 minutes, so each truck can only be used twice in one hour. That should mean that we need 2 trucks, so I test for that.

Note

The calculation might seem a bit strange so far because too few factors are taken into account and I just tried to get started with it. The important thing here isn't the calculation itself, but the process of how to move forward with TDD, so please don't let the calculation itself distract you.


When I try to compile the Tests project, it fails because the new test expects three new properties. Let's add those properties to the TRuckCalculation class. They could look like this:

//TruckCalculation public int TruckCapacity = 0; public int MillingCapacity = 0; public int UnloadingTime = 0;


Hmmm, that wasn't even properties, just public fields. We'll discuss this later. Now we can build the solution, and hopefully we'll now get a red bar.

Yes, expected and wanted. We need to make a change to NeededNumberOfTrucks, to make a real (or, at least, more real) calculation.

//TruckCalculation     public int NeededNumberOfTrucks     {         get         {            return MillingCapacity /            (TruckCapacity * 60 / UnloadingTime);         }     }


I now realize that this was perhaps too big a leap to take here, but I'm feeling confident now.

Let's build again and then re-execute the tests. Green bar; oops...red. How can that be? Ah, it wasn't the last test that was the problem; that test runs successfully. It's the old test that now fails. I get an exception from it. Of course, the first test doesn't give any values to the truckCapacity and UnloadingTime properties, so I get a division by zero. A silly mistake to make, but I'm actually very happy about that red bar. Even that tiny little first test helped me by finding a bug just minutes (or even seconds) after I created the bug, and this extremely short feedback loop is very powerful.

I need to change the calculation a bit more:

//TruckCalculation     public int NeededNumberOfTrucks     {         get         {             if (TruckCapacity != 0 && UnloadingTime != 0)                 return MillingCapacity /                 (TruckCapacity * 60 / UnloadingTime);             else                 return 0;         }     }


So we build, run the tests, and get a green bar. Goodanother step in the right direction, secured (at least to a certain degree) with tests.

Any refactorings? Well, I'm pretty sure several of you hate my usage of the public fields. I used to hate them myself, but I have since changed my mind. As long as the fields are both readable and writable and no interception is needed when reading or writing the values, the public fields are at least as good as properties. The good thing is that they are simpler; the bad thing is if you need to intercept when one of the values is set. I will refactor that later, if and when necessary, and not before.

Note

There is a difference between public fields and public properties when it comes to reflection, and that might create problems for you if you choose to switch between them.

A reviewer pointed out another difference that I didn't think about: the fact that public properties can't be used as ref arguments, but that's possible with public fields.

This whole discussion is also language dependent. In the case of C#/ VB.NET, a public field and public property is used the same way by the consumer, but that's not the case with C++ and Java, for example.


Another thing I could refactor is to add a [SetUp] method to the test class that instantiates a calculation member on the instance level. That's right; don't forget about your tests when you think about refactoring. Anyway, I can't say I feel compelled to do that change either, at least not right now. It would reduce duplication a little bit, but also make the tests slightly less clear. In the case of tests, clarity is often a higher priority.

Yet another thing that I'm not very happy about is that the code for the calculation itself is a bit messy. I think a better solution would be to take away what I think will be the least common situation of zero values in a guard. In this way, the ordinary and real calculation will be clearer. This change is not extremely important and is more a matter of personal taste, but I think it makes the code a little more readable. Anyway, let's make the refactoring called Replace Nested Conditional with Guard Clauses [Fowler R]:

//TruckCalculation     public int NeededNumberOfTrucks     {         get         {            if (TruckCapacity == 0 || UnloadingTime == 0)                return 0;            return MillingCapacity /            (TruckCapacity * 60 / UnloadingTime);         }     }


Note

Just like pattern names, refactoring names can be used as a means of communication among developers.


Build the tests and then run them. We still see green bars, and the code is clearer, but we can do better. I think it's a good idea here to use Consolidate Conditional Expression [Fowler R] like this:

//TruckCalculation     public int NeededNumberOfTrucks     {         get         {             if (_IsNotCalculatable())                    return 0;             return MillingCapacity /             (TruckCapacity * 60 / UnloadingTime);         }     }


And while I'm at it, the formula is a bit unclear. I reveal the purpose better if I change part of it into a property call instead by using Extract Method refactoring [Fowler R] like this:

public int NeededNumberOfTrucks {     get     {         if (_IsNotCalculatable())             return 0;         return MillingCapacity /         _SingleTruckCapacityDuringOneHour;     } }


That's fine for now, but it's time to add another test. However, I think this little introduction to writing a new class for calculating the needed number of trucks was enough to show the flow of TDD.

Some of you might dislike that you don't get help from intellisense (which helps you cut down on typing by "guessing" what you want to write) when writing tests first. I'm fond of intellisense too, but in this case I don't miss it. Remember, what we are talking about here is the interface, and it could be good to write it twice to get an extra check.

Also note that you should vary your speed depending upon how confident you are with the code you are writing at the moment. If it turns out that you are too confident and too eager so that you get into hard problems, you can slow down and write smaller tests and smaller chunks to get back to moving forward.

Design Effects

During the demo I said that writing the tests was very much a design activity. Here I have listed some of the effects I have found when designing with TDD instead of detailed up-front design:

  • More client control

    I used to think that as much as possible should be hidden from the outside regarding configuration so that classes configure themselves. To use TDD, you need to make it possible for the tests to set up the instances the way they need them. This is actually a good thing because the classes become much easier to reuse. (Of course, watch out so you don't open up the potential for mistakes.)

  • More interfaces/factories

    In order to make it possible (or at least easier) to use stubs or mock objects, you will find that you need to gather functionality via interfaces so you can pass in test objects rather than the real objects, which might be hard to set up in the test environment. If you come from a COM background, you will have learned the hard way that working with interfaces has many other merits.

Note

There will be more discussion about stubs and mocks later on in the chapter. For now, let's say that stubs are "stand-ins" that are little more than empty interfaces that provide canned values created for the sake of being able to develop and test the consumers of the stubs. Mocks are another kind of "stand-ins" for test purposes that can be set up with expectations of how they should be called, and afterward those expectations can be verified.


  • More sub-results exposed

    In order to be able to move a tiny step at a time, you need to expose sub-results so that they are testable. (If I had continued the demo, you would have seen me expose more sub-results for the transportation and load time, for example.) As long as the sub-results are just read only, this is not such a big deal. It's common that you will need to expose sub-results in the user interface anyway. Just be careful that you don't expose too much of your algorithms. Balance this carefully against the ordinary target of information hiding.

  • More to the point

    If I had started with detailed up-front design for the demo example, I'm pretty sure that I would have invented a truck class holding onto loads of properties for trucks. Because I just focused on what was needed to make the tests run, I skipped the truck class completely. All I needed that was closely truck-related was a truck capacity property, and I kept that property directly on the truck calculation class.

  • Less coupling

    TDD yields less coupling. For instance, because UI is hard to test with automatic tests, the UI will more or less be forced not to intermingle with the Domain Model. The coupling is also greatly reduced thanks to the increased usage of interfaces.

As I have already stated, the design effects are not the only good things. There are loads of other effects, such as

  • Second version of API

    When you use TDD, the API has already been used once when the time comes for your consumer to use it. Probably, the consumer will find that the API is better and more stable than it would have been otherwise.

  • Future maintenance

    Maintenance goes like a dream when you have extensive test suites. When you need to make changes, the tests will tell you immediately if it breaks on consumers.

    If you don't have extensive test suites when you need to make changes, you can go ahead and create tests before making the changes. It's not much fun, and you probably won't be able to create high-quality tests long after the code was written, but it's often a better approach than just making the changes and crossing your fingers. You then have tests when it's time for the next change, and the next.

    Despite using TDD, bugs will appear in production code. When it happens and you are about to fix the bug, the process is very similar to the ordinary TDD one. You start by writing a test that exposes the bug, and then write the code that makes the tests (the new and all the old tests) execute successfully. Finally, you refactor if necessary.

  • Documentation

    The tests you write are high-quality documentation. This is yet another good reason for writing them in the first place. Examples are really helpful in order to understand something, and the tests are examples both of what should work and what shouldn't.

    Another way of thinking about the unit tests is that you reveal your assumptions, and that goes for the other developers, too. I mean, they reveal their assumptions about your class. If their tests fail, it might be because your class isn't behaving as is expected, and this is very valuable information to you.

  • A smarter compiler

    In the past I have tried hard to make the compiler help me by telling me when I do something wrong. Tests are the next level up from that. The tests are pretty good at finding out what the compiler misses. Another way of stating it is that the compiler is great at finding syntax problems, and the tests will find semantic problems.

    It's even the case that you probably will value type safety a little less when you are using TDD because tests will catch type mistakes easily. That in its turn means that you can take advantage of a more dynamic style.

    If you follow the ideas of TDD, you will also find that you don't need to spend nearly as much time with the debugger as you would otherwise have to.

Note

Even so, there might be situations where you need to inspect the values of variables, and so on. You can use Console.WriteLine() calls, and you can run the tests within the debugger. Personally I think it's a sign of too big leaps or too few tests when I get the urge to use those techniques.


  • Reusability chance in other tests

    The tests you write during development will also come in handy for the integration tests. In my experience, many production time problems are caused because of differences in the production environment compared to what is expected. If you have an extensive set of tests, you can use them for checking the deployment of the finished application as well. You pay once, but reap the benefits many times over.

To summarize it all, test-friendly design (or testability) is a very important design goal nowadays. It's perhaps the main factor when you make design choices.

Problems

I have sounded like a salesman for a while. However, there are problems too. Let's have a look at the following:

  • UI

    It is hard to use TDD for the UI. TDD fits easier and better with the Domain Model and the core logic, although this isn't necessarily a bad thing. It helps you be strict in splitting the UI from the Domain Model, which is a basic design best practice (so it should be done anyway, but TDD stimulates it). Ensure you write very thin UI and factor out logic from the forms.

  • Persistent data

    Databases do cause a tricky problem with TDD because once you have written to the database in one test, the database is in another state when it's time for the next test, and this causes trouble with isolation. You could write your code to set up the database to a well-known state between each test, but that adds to the tester's burden and the tests will run much more slowly.

    You could also write tests so that they aren't affected by the changed state, which might reduce the power of the tests a bit. Yet another solution is to use stubs or mock objects to simulate the database apart from during the integration tests when a backup of the database is restored before the tests.

    Yet another way is to use transactions purely for the sake of testing. Of course, if your tests focus on the transactional semantics, this isn't a useful solution.

Note

We will discuss database bound testing more in Chapter 6, "Preparing for Infrastructure."


  • False sense of security

    Just because you get a green bar doesn't mean that you have zero bugsit just means that your tests haven't detected any bugs. Make sure that TDD doesn't lead you into a false sense of security. Remember that a green test doesn't prove the absence of bugs, only that that particular test executed without detecting a problem.

    On the other hand, just because your tests aren't perfect doesn't mean that you shouldn't use them. They will still help you quite a lot with improving the quality!

    As my friend Mats Helander once said, the value isn't in the green, but in the red!

  • Losing the overview

    TDD can be seen as a bottom-up approach, and you must be aware that you might lose the overview from time to time. It's definitely a very good idea to visualize your code from time to time as UML, for example, so you get a bird's eye perspective. You will most certainly find several refactorings that should be applied to prepare your code for the future.

  • More code to maintain

    The maintenance aspect is a double-edged sword. The tests are great for doing maintenance work, but you also have more code to maintain.

I just said that TDD is hard to apply for the UI and database, and that's just one more reason why the Domain Model pattern shines because you can typically run more meaningful tests without touching the database and the UI compared to when other logical structures have been used! That said, we will discuss both database testing and UI testing in later chapters.

Anyway, all in all I think TDD is a great tool for the toolbox!

The Next Phase?

That was an introduction to the basic concepts, but we have only scratched the surface a little bit. When you start applying TDD, it will probably work smoothly after you pass those first start-up problems.

Then after a while you will probably find that setting up some tests gets overly complex and too dependent on uninteresting partsuninteresting for the test itself at least. That's a good sign that you have reached the next phase of TDD. My friend Claudio Perrone will talk more about that in the next section called mocks and stubs.




Applying Domain-Driven Design and Patterns(c) With Examples in C# and  .NET
Applying Domain-Driven Design and Patterns: With Examples in C# and .NET
ISBN: 0321268202
EAN: 2147483647
Year: 2006
Pages: 179
Authors: Jimmy Nilsson

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net