Anyone who has done software development for any appreciable amount of time has run across code that can only be described as looking like the original developer was on heavy doses of cold medicine when he or she wrote it. And we usually complain about how horrid the code is and we laugh about it (unless we’re the guilty party) but we almost never repair it. Why not? Because many of us have suffered the result of modifying code even in seemingly innocuous ways. Seemingly innocent and inconsequential changes can cause nasty side-effects. Worse than that those side-effects may not be apparent till the next time the user runs the app.

But what if there were a way we could change the code but still feel confident that we won’t break anything? We use version control so we can modify code we’re in the process of creating without having to worry about inadvertently introducing a breaking change. What if we had something that would give us that same kind of safety net to prevent breaking working code?

There is such a technique: automated testing. Of course, most of us never have time to test–much less build automated tests. Like documentation, tests are often jettisoned when a deadline is looming. Even if testing is a top priority, we still don’t have unlimited time or resources to test our code. Businesses have priorities and “the code must be 100% bug free” isn’t usually at the top of the list.

What if instead of waiting till the last minute to try to test, we wrote the tests before we write code? This, in essence, is the notion of TDD and this is why it’s beneficial. It gives us the safety net we need in order to clean up our code while being able to be confident that we’re not potentially setting ourselves up for 2 am calls from frantic operations folks. In TDD we first write the failing test, write the minimum code to make the test pass and finally refactor the code into something that we’re proud to call our own.

I am not going to discuss unit testing or continuous integration to any extent even though they are related concepts. Suffice it to say that it’s worth your while to study both of those ideas as well.

A REAL CONTRIVED EXAMPLE

I find it harder to understand concepts which are presented in a totally abstract fashion; concrete examples make it easier for me. So I think it’d be helpful to work through an example of TDD.

A word on tooling: while there are several tools available for unit testing and TDD on the .Net platform, I am going to use MSTest for this example. This isn’t an endorsement of MSTest; it’s just the easiest to use because it comes pre-installed with Visual Studio.

Ok, so we work for the Carpentry Consortium Of America and members of the CCA are forever annoyed by having to do calculations on fractions. They want us to help them out by building a fraction calculator for them.

You, being the diligent and professional developer that you are ask them to give you requirements. They, being the human beings that they are say–“Yeah, we need a calculator for fractions. Oh and when can you have it done?”

At this point you might be inclined to bemoan the fate of the poor software developer in this world. But instead you decide to try to work with your users (a novel idea) to figure out what it is that they really need:

You: Does it need to be able to add fractions?

They: Of course we need it to be able to add fractions!

You: What should happen if someone inputs a bad fraction? For instance 1/0?

They: That will never happen. (You make a mental note that because they said “That will never happen,” that’s exactly what will happen and you need to test for it).

So at this point you’ve got two basic requirements; you need to add fractions and you need to trap for bad fractions being entered. Rather than torture yourself further, you decide to go off and work on those two use cases as your first sprint. For sake of easy reference, let’s call them R0 (Requirement 0) and R1.

R0: If a bad fraction is entered, stop the calculation.

R1: Add two valid fractions and return the correct result.

RO

First let’s work through the process for R0. First I write a test:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
        
namespace RatNumCalculator.Test  //Note 1
{
    [TestClass]  //Note 2
    public class RationalNumberCalculatorTests
    {
        [TestMethod]  //Note 2
        [ExpectedException(typeof(ArgumentOutOfRangeException))]  //Note 3
        public void AnInvalidRationalNumberWillThrowAnException() //Note 4
        {
            var validNumerator = 1;
            var invalidDenominator = 0;
            var r = new RationalNumber(validNumerator, invalidDenominator);
         }
    }
}

Note a few things here:

1.) We’ve identified our namespace as a test namespace. This isn’t a coding issue so much as it is a readability issue. It’s easier for other developers to read and understand our work if we follow certain conventions.

2.) We decorate our test class and our test method with special attributes. This is a requirement of MSTest but other unit test frameworks have similar mechanisms.

3.) We specify the behavior that we expect from the code. In this case we expect that if we try to specify an invalid rational number we’ll get an ArgumentOutOfRangeException thrown. If the exception isn’t thrown the test fails.

4.) We give the test a descriptive name. That way if it fails, on the failure report we’ll have a good idea of what it is that got broken.

Now when we attempt to run this test, it’s going to fail immediately. That’s because it won’t compile; we can consider the inability to compile a failure for this purpose. So we must write code to get the test at least to the point that we can compile it.

using System;

namespace RatNumCalculator
{
    public class RationalNumber
    {        
        public int numerator { get; set; }
        public int denominator { get; set; }
        public RationalNumber(int n, int d)
        {
            numerator = n;
            denominator = d;
         }
    }
}

This is, of course, the smallest code which I can write to compile the test. And when I run the test, the test fails. It’s important to write a test which fails first.

Why is it important to write a test which is guaranteed to fail the first time we run it?

1.) We want to make sure that we don’t accidentally write a test which always passes–such a test would be worthless.

2.) We want to insure that our test harness is executing the test as we expect.

3.) This also guards against the (relatively unlikely at first) case of accidentally duplicating existing functionality.

So now that we’ve got a basic, failing test, let’s add code to make the test pass for our first requirement. Note, we’re only adding code sufficient to make the test pass and no more.

private int _denominator;
public int numerator { get; set; }
public int denominator
{
    get { return _denominator; }
    set 
    { 
        if (value == 0)
        {
            throw new ArgumentOutOfRangeException();
        }
        else
        {
            _denominator = value;
        }
    }
}

When I re-run the test with this code added, the test now passes. So I now have code to satisfy R0. From here on out whenever I change the code I can run the tests and be assured that I haven’t broken the code which satisfies R0.

R1

Now I need to write code to test R1. Just for sake of simplicity, I’m going to test adding 1/2 and 1/4.

First the test:

[TestMethod]
public void AddingTwoValidRationalsShouldReturnTheCorrectResult()
{
    var validRationalOne = new RationalNumber(1,2);
    var validRationalTwo = new RationalNumber(1,4);

    var result = validRationalOne.add(validRationalTwo);
    var expected_result = new RationalNumber(3,4);

    Assert.AreEqual(result.ToDecimal(), expected_result.ToDecimal(), 0.000001);  //Note 1
}

Note 1: Assert.AreEqual allows us to pass a “delta” argument when dealing with doubles. This allows us to account for the imprecise nature of how doubles are stored so we don’t get a false negative on the test.

I can’t attempt to run the test immediately because the code will not compile. So I first write only enough code to pass the test; in this case enough code to allow me to compile. This:

public RationalNumber add(RationalNumber n)
{
    return new RationalNumber(1,1);
}

Ok, so now I need to write code to add two rational numbers. Here’s the additional code:

public RationalNumber add(RationalNumber n)
{
    //1.) Figure out least common multiple of this and n
    var lcm = LeastCommonMultiple(denominator, n.denominator);
    //2.) Transform both this and n to have the the same denominator
    var r1 = new RationalNumber(numerator * (lcm / denominator), denominator * (lcm / denominator));
    var r2 = new RationalNumber(n.numerator * (lcm / n.denominator), n.denominator * (lcm / n.denominator));
    var r3 = new RationalNumber((r1.numerator + r2.numerator), r1.denominator);
    return r3;
}

private int LeastCommonMultiple(int n, int m)
{
    return (n * m) / GreatestCommonDivisor(n, m);
}

public int GreatestCommonDivisor(int a, int b)
{
    while (a != 0 && b != 0)
    {
        if (a > b)
            a %= b;
        else
            b %= a;
    }

    if (a == 0)
        return b;
    else
        return a;
}

public double ToDecimal()
{
    return numerator/denominator;
}

Now I run my test again and now I have two passing test cases.

Some of the more mathematically inclined readers may have spotted a little something in my add method that should be changed. This calculation of the new denominator specifically:

new RationalNumber(numerator * (lcm / denominator), denominator * (lcm / denominator));

denominator * (lcm / denominator) is simply lcm. Hence I could change the code (and make it a bit simpler too) like so:

new RationalNumber(numerator * (lcm / denominator),  lcm);

But how could I be sure my math is right? (In this case I’m sure my math is right but this is also a contrived example. In actual practice it’s not usually so simple to see where someone’s made a flub in code and even if you can you often don’t have tests to insure nothing else got broken) Easy–make the code change and run the test. And that’s the whole benefit of Test Driven Development. I can fix those sorts of warts in the code without having to worry about potentially breaking working code.

I hope this helps other developers to understand why TDD is such a great practice to adopt and how to adopt it to their own work.