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 FlowFirst, 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 DemoI'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 EffectsDuring 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:
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.
As I have already stated, the design effects are not the only good things. There are loads of other effects, such as
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.
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. ProblemsI have sounded like a salesman for a while. However, there are problems too. Let's have a look at the following:
Note We will discuss database bound testing more in Chapter 6, "Preparing for Infrastructure."
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. |