Unit testing 5: test-driven development

Tags:

In this post I’ll introduce the methodology known as test-driven development, and how to use it in your projects.

The difference between “normal” and test-driven development (TDD) is that when doing TDD, you write unit tests for your new code before writing the code itself. This way you ensure good test coverage for your code, and your code will also be more flexible and reusable, as you have to design the class interfaces for easy testing.

Test-driven development basics

As mentioned, the main idea is that before you write any actual application code, you write a unit test(s) which checks the code you will be writing.

It may initially be a bit difficult to adjust to writing the tests before any code. How do I know what to test when I don’t have any code?

The idea is not so difficult. Think of how you want to use the code, and write a test based on that. If you need to write a class which sums numbers, it needs a sum method and when passed two numbers the output is their sum.

Preconditions and postconditions

Another good approach is to think of preconditions and postconditions. This is something you can think of when writing unit tests in general, too.

  • A precondition is something that must be true before something is done
  • A postcondition is something that must be true after something is done

In tests we care more about the postconditions, but some things can be thought of as preconditions as well: When you create a new object, its properties must meet certain criterias, such as a collection should be empty. While this is actually a postcondition of the constructor, it may be easier to think of it as a precondition for any other operations.

Writing code in a TDD manner

Now it’s time to look at a simple example of writing code using TDD. We will first need some goal for our code.

Let’s say we’re writing a sales system and we need a class for orders. Orders need to have items in them, a recipient, order IDs etc.

We don’t have any code yet, and let’s keep it that way, as first we need to write tests. Let’s decide that an order must be empty and it must have no recipient when it’s created:

class OrderTest extends PHPUnit_Framework_TestCase {
    public function testOrderStartsEmpty() {
        $order = new Order();
        $this->assertEquals(0, count($order->getItems()));
    }
 
    public function testOrderStartsWithoutRecipient() {
        $order = new Order();
        $this->assertNull($order->getRecipient());
    }
}

Now we have tests that confirm the correct behavior of the constructor – we could say the postconditions of the constructor are that item count is 0 and recipient is null.

Of course, when running the above test it will fail quite miserably. We have no class, no methods, nothing. This is a good thing, as now we know that the test actually works as expected.

We can now write the Order class with the methods required by the test:

class Order {
    private $_items = array();
    private $_recipient = null;
 
    public function __construct() {
    }
 
    public function getItems() {
        return $this->_items;
    }
 
    public function getRecipient() {
        return $this->_recipient;
    }
}

And now the tests we wrote pass. Congratulations, now you have written code using test-driven development!

Another example

Now, we need to be able to add items to our order. To keep things simple for the example, the items are just going to be strings such as 'Cat food' or 'Bananas'.

Again, let’s define the behavior of the code first, then write tests and then the actual code. We’ll add a method called addItem, which takes the item as its parameter. If the parameter is not valid, it’ll throw an exception.

public function testHasItemAfterAdding() {
    $order = new Order();
 
    $this->assertFalse($order->hasItem('Cat food'));
 
    $order->addItem('Cat food');
 
    $this->assertTrue($order->hasItem('Cat food'));
}
 
public function testAddingInvalidItemThrowsException() {
    $order = new Order();
 
    try {
        $order->addItem('');
        $this->fail('Empty items are invalid');
    }
    catch(InvalidArgumentException $ex) {
    }
}

If we run the tests again, they should fail. Now we could write the code to make the tests pass, but I will leave that as an exercise to you.

In closing

Test-driven development is not as difficult as it may initially seem. You only need to adjust your thinking a bit.

You may notice that in our tests and code above we didn’t have good tests for some of the methods like getItems and hasItem. I left them out as it would’ve made the post unnecessarily long, but when writing actual code you would want to write tests for them as well. Because of such small helper methods, it may sometimes be a good idea if you have a class diagram or if you write empty stub methods in your code before writing the tests. This will help remind you of what methods you have to test.

Next time we’ll look at testing a Zend Framework application.