23 January 2010

Test Driven Development Applied [part 1]


Introduction

Today I’m starting a series of articles about test driven development. By reading blogs about this subject I realised the lack of simple examples on how to apply TDD properly when creating a new entity. I’ll be starting by defining an interface for a song (could have been something else but I was listening to music while coding) then write tests following some acceptance criteria.

I’ll be using Microsoft unit testing libraries with Visual Studio 2008 but you could also use NUnit. I’ll be coding using C#.

I won’t be commenting much or using regions in my code otherwise this article would be unreadable. I’m not a big fan of comments anyway.

The Song entity

Let me define some acceptance criteria before I can start coding:

A song should have an id greater than zero and a name that cannot be null or an empty string. An exception should be thrown if one of these criteria is not met. The properties should only be set by the constructor ensuring that the entity is in a valid state when instantiated.

Before I write any tests I know that the Song interface should look like the following:

public interface ISong
{
    int Id { get; }

    string Name { get; }
}

Write failing Test -> Make the test pass -> Refactor -> ReRun test


First of all there are people that like to test more than one thing in a test method. When you start using TDD you often don’t really know what to test first so it is better to be very specific about what is being tested.

It’s often a good idea to make a list of the tests that you can think of to get started. Because the acceptance criteria are very clear I can make a nice list.

Test 1: The constructor should throw an exception when passing a negative id.
Test 2: The constructor should throw an exception when passing zero as the id.
Test 3: The constructor should throw an exception when passing a null name.
Test 4: The constructor should throw an exception when passing an empty string as the name.
Test 5: The constructor should set the Id property with the id parameter.
Test 6: The constructor should set the Name property with the name parameter.


Let’s write the first test.

[TestClass]
public class SongTests
{
    [TestMethod]
    public void Constructor_NegativeIdParameter_ThrowsException()
    {
        try
        {
            var song = new Song(-1, "my song name");
            Assert.Fail();
        }
        catch (ArgumentException)
        {
        }
    }
}

I use the following notation for the name of the test methods:

[Method being tested]_[Condition to get the expected result]_[Expected result]


By having a first failing test, I can start writing part of the Song class.

public class Song : ISong
{
    public int Id { get; private set; }

    public string Name { get; private set; }

    public Song(int id, string name)
    {
        if (id < 0)
        {
            throw new ArgumentException();
        }
    }
}

The test is now succeeding with the minimum implementation.

The second test:

[TestMethod]
public void Constructor_ZeroIdParameter_ThrowsException()
{
    try
    {
        var song = new Song(0, "my song name");
        Assert.Fail();
    }
    catch (ArgumentException)
    {
    }
}

The test is failing. We need to modify the constructor a little bit:

public class Song : ISong
{
    public int Id { get; private set; }

    public string Name { get; private set; }

    public Song(int id, string name)
    {
        if (id <= 0)
        {
            throw new ArgumentException();
        }
    }
}

We have another successful test.

From here we can start refactoring the test class to get rid of the duplication.

[TestClass]
public class SongTests
{
    [TestMethod]
    public void Constructor_NegativeIdParameter_ThrowsException()
    {
        ThrowsArgumentException(() => new Song(
            -1,
            "my song name"));
    }

    [TestMethod]
    public void Constructor_ZeroIdParameter_ThrowsException()
    {
        ThrowsArgumentException(() => new Song(
            0,
            "my song name"));
    }

    private static void ThrowsArgumentException(Action action)
    {
        try
        {
            action();
            Assert.Fail();
        }
        catch (ArgumentException)
        {
        }
    }
}

Run the two tests again to make sure they still pass.

We can now test that the constructor checks that the name parameter is not null.

[TestMethod]
public void Constructor_NullNameParameter_ThrowsException()
{
    ThrowsArgumentException(() => new Song(
        1,
        null));
}

Modify the constructor accordingly.

public Song(int id, string name)
{
    if (id <= 0)
    {
        throw new ArgumentException();
    }

    if (name == null)
    {
        throw new ArgumentException();
    }
}

The test should pass and we can get rid of the duplication in the constructor as well as extracting complexity to different methods.

public class Song : ISong
{
    public int Id { get; private set; }

    public string Name { get; private set; }

    public Song(int id, string name)
    {
        GreaterThanZeroIntChecker(id);
        NonNullStringChecker(name);
    }

    private static void GreaterThanZeroIntChecker(int parameter)
    {
        ThrowArgumentExceptionIfTrue(parameter <= 0);
    }

    private static void NonNullStringChecker(string parameter)
    {
        ThrowArgumentExceptionIfTrue(parameter == null);
    }

    private static void ThrowArgumentExceptionIfTrue(bool condition)
    {
        if (condition)
        {
            throw new ArgumentException();
        }
    }
}

Run the tests.

The Song class starts to look better. Let’s write a test for the constructor to throw an exception when passing an empty string as the name parameter.

[TestMethod]
public void Constructor_EmptyNameParameter_ThrowsException()
{
    ThrowsArgumentException(() => new Song(
        1,
        string.Empty));
}

We can modify the constructor and the NonNullStringChecker method.

public class Song : ISong
{
    public int Id { get; private set; }

    public string Name { get; private set; }

    public Song(int id, string name)
    {
        GreaterThanZeroIntChecker(id);
        NonNullOrEmptyStringChecker(name);
    }

    private static void GreaterThanZeroIntChecker(int parameter)
    {
        ThrowArgumentExceptionIfTrue(parameter <= 0);
    }

    private static void NonNullOrEmptyStringChecker(string parameter)
    {
        ThrowArgumentExceptionIfTrue(string.IsNullOrEmpty(parameter));
    }

    private static void ThrowArgumentExceptionIfTrue(bool condition)
    {
        if (condition)
        {
            throw new ArgumentException();
        }
    }
}

Make sure the tests still pass.

Now that we’ve tested the parameters of the constructor we can check that the Id property gets set properly.

[TestMethod]
public void Constructor_ValidParameters_SetsIdProperty()
{
    // Arrange
    int id = 1;

    // Act
    var song = new Song(id, "my song name");

    // Assert
    Assert.AreEqual(id, song.Id);
}

The test fails as expected so modify the constructor.

public Song(int id, string name)
{
    GreaterThanZeroIntChecker(id);
    NonNullOrEmptyStringChecker(name);

    this.Id = id;
}

The tests should pass. Let’s write the last test that checks that the constructor sets the Name property.

[TestMethod]
public void Constructor_ValidParameters_SetsNameProperty()
{
    // Arrange
    string name = "my song name";

    // Act
    var song = new Song(1, name);

    // Assert
    Assert.AreEqual(name, song.Name);
}

The test fails. Modify the constructor as follow.

public Song(int id, string name)
{
    GreaterThanZeroIntChecker(id);
    NonNullOrEmptyStringChecker(name);

    this.Id = id;
    this.Name = name;
}

All the tests should succeed. We now have all the tests that we need to meet the acceptance criteria defined in the introduction.

From here you could actually say that you have done enough but I personally prefer to randomise the values that I use for my tests. It would be nice if I could just use the Random class and call methods on it to get a random int and a random string. The solution is to write extension methods for the Random class.
The extension methods will only be used for testing purpose. I believe there is no need to test methods that are not used in your production source code and that are only there to generate random values.

public static class RandomExtensions
{
    public static string NextString(this Random random)
    {
        return Guid.NewGuid().ToString();
    }

    public static int NextGreaterThanZero(this Random random)
    {
        return random.Next(1, int.MaxValue);
    }
}

Here is the final version of the test class:

[TestClass]
public class SongTests
{
    private Random random = new Random();

    [TestMethod]
    public void Constructor_NegativeIdParameter_ThrowsException()
    {
        ThrowsArgumentException(() => new Song(
            -1,
            this.random.NextString()));
    }

    [TestMethod]
    public void Constructor_ZeroIdParameter_ThrowsException()
    {
        ThrowsArgumentException(() => new Song(
            0,
            this.random.NextString()));
    }

    [TestMethod]
    public void Constructor_NullNameParameter_ThrowsException()
    {
        ThrowsArgumentException(() => new Song(
            this.random.NextGreaterThanZero(),
            null));
    }

    [TestMethod]
    public void Constructor_EmptyNameParameter_ThrowsException()
    {
        ThrowsArgumentException(() => new Song(
            this.random.NextGreaterThanZero(),
            string.Empty));
    }

    [TestMethod]
    public void Constructor_ValidParameters_SetsIdProperty()
    {
        // Arrange
        int id = this.random.NextGreaterThanZero();

        // Act
        var song = new Song(id, this.random.NextString());

        // Assert
        Assert.AreEqual(id, song.Id);
    }

    [TestMethod]
    public void Constructor_ValidParameters_SetsNameProperty()
    {
        // Arrange
        string name = this.random.NextString();

        // Act
        var song = new Song(this.random.NextGreaterThanZero(), name);

        // Assert
        Assert.AreEqual(name, song.Name);
    }

    private static void ThrowsArgumentException(Action action)
    {
        try
        {
            action();
            Assert.Fail();
        }
        catch (ArgumentException)
        {
        }
    }
}

Your test code should be of the same quality as your production code so that you can easily make a change when needed. It takes a bit longer to write production code using TDD but it is worth the investment on the long run. TDD gives a safety net which makes you confident when changing your production code. It is also a way to document it. You should be able to read tests and understand what’s happening.

My next article will be about mocking objects using Rhino mocks.

I hope this article will be useful to someone out there on the web.

No comments:

Post a Comment