Home > Craftsmanship > Test-Driven Development

Test-Driven Development

As software developers, we’re constantly striving to find ways to improve our code, speed up our development time, and generally reduce the stress that comes from developing large systems. In recent years, I’ve been studying and practicing many different techniques to find out what works best for me, and one of my most successful endeavors has been the introduction of Test-Driven Development (TDD) into my tool box.

Wikipedia defines TDD1 as a software development technique consisting of short iterations where new test cases covering the desired improvement or new functionality are written first, then the production code necessary to pass the tests is implemented, and finally the software is refactored to accommodate changes. The availability of tests before actual development ensures rapid feedback after any change.

Uh, yeah, that sounds great. Let me summarize Uncle Bob Martin2 to make the benefits of TDD a little clearer. First off, TDD can greatly reduce the need you have for a debugger. When you’re doing TDD right, you know almost instantly when you’ve broken some code, and you know exactly where the break occurred. You don’t need to spend time tracing through call stacks to discover a small logic problem in your switch statements. Secondly, over the course of developing a product, you generate tons of small tests that can be run at a moment’s notice. This gives you a way to ensure that any new code you put into the system doesn’t break any old code already in place. These “regression tests” give us the confidence to clean up old code (refactor), safe in the knowledge that we’re not breaking anything in process. Third, all those tests provide example code of how the production code actually works. You need to figure out the proper way to instantiate an object? Find that object’s unit tests for the answer. Because of the very nature of TDD, those examples are always up-to-date, unlike all those documentation artifacts that you’ve produced which nobody ever reads. Lastly, Uncle Bob points out that because of the very small steps we take when doing TDD, our code has a lower degree of coupling, and much high cohesion. Because of this last point, long-time practitioners of TDD claim that it leads to better designed software.

Don’t get me wrong, TDD isn’t a magic bullet. You’re not going to suddenly start writing flawless software because of TDD. TDD can’t replace the need for strong object-oriented design skills, continuous integration, automated build processes, and so forth. However, when you couple that knowledge with the tenets of TDD, wonderful things can happen.

Getting Started With TDD

If you’ve heard of TDD before, you’ve probably heard the TDD mantra, “Red. Greeen. Refactor.” These are three core steps of TDD, and they go something like this.

  1. Write a failing unit test.
  2. Write just enough production code to pass that one test.
  3. Refactor your code (remove duplication).

The first step is the hardest. We’ve got to pick a feature of our system and write a small test to ensure it works…even though we’ve not actually written any of the code yet. Obviously, this unit test is going to fail. When we run our test suite, our failing test will be indicated by a red bar. That’s good. That’s where we want to start. Our next step is to write the production code necessary to get our test to pass. Once we’ve done this, running our test suite again will show us that we’re now passing our tests, and all passing tests are indicated by a green bar. Our last step is to remove duplication from our code, be it the production code or the test suite. This is the refactoring step, and after we’ve completed this step, we run our test suite again to ensure that our tests still pass.

Let’s take a look at an example. This example is pretty much ripped right out of Kent Beck’s book, Test-Driven Development: By Example3. In our example, we want to create a system to allow us to track money and perform various operations on that money. We’re going to start by creating an object to hold our money. We can’t create that object yet, because we don’t have a failing test.

So, step one, create a failing test.

class DollarTest
{
    public function testDollarInstantiation()
    {
        $five = new Dollar(5);
        $this->assertEquals(5, $five->amount);
    }
}

If we run our test suite, this test fails. Actually, this won’t even compile, so we need to add a little bit of code just to get us to a failing test.

class Dollar {}

Now, we can run our test again and we successfully compiled AND got a failing test!! That’s exactly what we wanted. Now we can write the production code necessary to pass that test.

class Dollar
{
    public $amount;
    public function __construct($amt)
    {
        $this->amount = $amt;
    }
}

We run our test one more time and we see our test pass. Yay!! We’ve got our green bar. We’re making great progress.

However, there are some problems with that that code. For one, do we really want to expose the amount as a public member variable? Probably not. Lets put a getter on that Dollar object so we can read the amount without having to expose the member variable.

class DollarTest
{
    public function testDollarInstantiation()
    {
        $five = new Dollar(5);
        $this->assertEquals(5, $five->amount);
    }

    <strong>public function testDollarAmountGetter()
    {
        $four = new Dollar(4);
        $this->assertEquals(4, $four->getAmount());
    }</strong>
}

We run the test suite and watch our new test fail. Awesome!

class Dollar
{
    public $amount;
    public function __construct($amt)
    {
        $this->amount = $amt;
    }

    <strong>public function getAmount()
    {
        return $this->amount;
    }</strong>
}

Once again, we run our tests and they both pass. We’re doing great. However, now it’s time to refactor some code. We want to turn our public member variable into a private member variable.

class Dollar
{
    <strong>private</strong> $amount;
    public function __construct($amt)
    {
        $this->amount = $amt;
    }

    public function getAmount()
    {
        return $this->amount;
    }
}

As always, we need to run our tests to make sure nothing fails. Lo and behold, we’ve broken our first test. We need refactor our first test to take this new design decision into account. This is an important part of TDD, we not only refactor our production code, but from time to time, we need to refactor our tests, too.

class DollarTest
{
    public function testDollarInstantiation()
    {
        $five = new Dollar(5);
        $this->assertEquals(5, <strong>$five->getAmount()</strong>);
    }

    public function testDollarAmountGetter()
    {
        $four = new Dollar(4);
        $this->assertEquals(4, $four->getAmount());
    }
}

One last time, we run our tests and note that all our tests are passing.

We’ve now successfully finished our first TDD cycle. While this is a very small, and very contrived, example, it illustrates a couple of important points. First and foremost, we REALLY do want to take extremely small steps. If we knew we were going to write a small object to hold dollar amounts, could we have come up with that design and coded it all in about 30 seconds without TDD? Of course, but keep this in mind. TDD isn’t necessarily about taking small steps, it’s about being able to take small steps. Ultimately, we shouldn’t have to spend hours programming a new feature before we have a chance to figure out if it’s going to work. We want our feedback loop to be measured in seconds.

Guidelines For TDD

In the above examples, I took extremely small steps. I can’t reiterate enough how important this is when you’re first getting started with TDD. Eventually you’ll feel much more comfortable taking slightly larger steps, but these small steps are vital when you’re first beginning. The following are some other things are important as you start to explore TDD.

Use a framework. xUnit has been ported to just about every language known to man. Unless you absolutely have to, don’t waste time writing your own test harnesses. Not to mention that your test harnesses need to be tested.

When writing your tests, make sure each test is only testing one thing. If you have a bunch of assert statements in your test cases, it can be tough to figure out which one of them is throwing the error. Ideally, you just want to test one thing in your unit test.

Your tests should run very quickly. If it takes five minutes for a test to run, how likely are you to actually run that test? That answer is, not likely. Make sure your tests are small and run quickly in order to ensure that the test actually does get run.

Refactoring isn’t limited to your production code. Sometimes you need to refactor your test cases. This is good and normal. Just remember to keep running that test suite to ensure nothing is broken.

If you’ve already got a red bar, don’t write more tests. Get that red bar to green first. If you keep writing tests, you’re going to be staring at a red bar for a long time, and it may be difficult to figure out where problems are once you start implementing the actual code. Until you’re more comfortable with TDD, stick to one test at a time.

Tests should not depend on the results of other tests. If you have one test rely on the results of a previous test in the suite, you’ve got tightly coupled tests, and we all know that’s a bad word. Similarly, the order your tests run shouldn’t matter.

Practice Tips

TDD, just like anything else, takes practice to get better. Ideally, any practice you do will be overseen by someone experienced with TDD, but by simply trying it out and soliciting feedback from your peers, you can improve. A technique I have used to great success is to pick a simple game (blackjack, chutes and ladders, etc.) and implement a basic game using TDD. This is most effective when you’ve got a small group all working at the same time. Ideally, the exercise will take about three to four hours. To get the most benefit from this exercise, compare notes with the other teams to see how they went about implementing the same project.

Choosing a game to implement can be a bit of a challenge. You want a game that is simple enough for everyone to understand the rules, but complicated enough that the exercise takes several hours. If the game is too simple, too many people tend to jump straight to the solution without going through the iterations that TDD takes you through. For instance, I tried this exercise once with a program to convert numbers between decimal and roman numeral. This was entirely too small, and too many people had already worked on this in school. The solution was simply too obvious.

Frequently Asked Questions

I’ve given my TDD presentation several times, and there are several questions that I almost always get asked. Here they are:

Q: Does doing TDD slow down development?

A: That has not been my experience. If you’re already writing unit tests, writing them first isn’t going to be any slower than writing them last, and you’re much more likely to actually write them using TDD. I know people that have been practicing TDD for many years, and they all claim that they develop software much faster with TDD than without.

Q: Do I REALLY have to take such small steps?

A: Again, it’s not necessarily about taking small steps, it’s about being able to take small steps. As you grow more comfortable with TDD, you’ll be able to take slightly larger steps. Don’t rush it, though. It’ll come.

Q: How do I decide when to stop writing tests for a method?

A: Your next test should tell you something about the system that you don’t already know. Don’t waste time writing a bunch of tests for your multiplication routine. Once you’ve tested that 5 * 2 = 10 and -10 * 4 = -40, is testing 6 * 7 = 42 going to tell you anything about the system that you didn’t already know? No, it isn’t. Unit tests definitely provide diminishing returns, so this is a good rule of thumb. The real answer is that you’re done writing tests for a particular method when you feel confident about that method.

Q: Can I do TDD with “legacy” projects, or just greenfield projects?

A: Not only CAN you do TDD with legacy projects, it’s the ONLY way I develop when working on legacy projects. There’s an entire book dedicated to the subject of getting legacy projects under test called “Working Effectively With Legacy Code” by Michael Feathers. I HIGHLY recommend this book.

Q: What tools do you use for working with TDD?

A: I pretty much always use the xUnit port for the language I’m working in. It’s just a really solid framework. However, if you’re working with a .NET language, Microsoft has built in their own test framework to Visual Studio. Quite frankly, they should have just integrated nUnit, but being Microsoft, they wrote their own. It’s not bad, and with it being built into the IDE, and combined with ReSharper, you get some pretty nifty tools for working with TDD.

Q: Does TDD work?

A: Well, it works for me, but it’s definitely not for everyone. At the end of the day, I whole heartedly believe that I write better software using TDD than by using a test last approach. By writing the tests first, I have a clearer idea of how objects get instantiated and wired up to other objects. I am much more focused on writing small, and generally, less coupled modules. Sure, I can do that in code anyway, but TDD almost forces it happen without me even having to think about it.


1. http://en.wikipedia.org/wiki/Test_Driven_Development
2. http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd
3. Test Driven Development: By Example @ Amazon

Categories: Craftsmanship Tags: