Thursday, September 24, 2009

Practical testing in php part 2: write clever tests

This is the second part of the testing series about php. You may want to subscribe to the feed to check out previous parts and not miss the next ones.

In the previous part, we have discovered the syntax and the infrastructure needed to run a test with phpunit. Now we are going to show a practical example using a test case/production code couple of classes.
What we are going to test is the Spl class ArrayIterator; for the readers that do not know this class, it is a simple Iterator implementation which abstracts away a foreach on the elements of an array.
Of course it would be very useful to write the tests before the production code class, but this is not the time to talk about TDD and its advantages: let's simply write a few tests to ensure the implementation works as we desire. This is also a common way to study the components of an object-oriented system: read and understand its unit test and write more of them to verify our expectations about the production classes behavior are fulfilled.
Let's start with the simplest test case: an empty array.
class ArrayIteratorTest extends PHPUnit_Framework_TestCase
{
    public function testEmptyArrayIsNotIteratedOver()
    {
        $iterator = new ArrayIterator(array());
        foreach ($iterator as $element) {
            $this->fail();
        }
    }
}
The test case class is named ArrayIteratorTest, following the convention of using a 1:1 mapping from production classes to test ones. The test method simply creates a new instance of the system under test, setting up the situation to have it iterate over the empty array. If the execution path enter the foreach, the test fails, as the call to fail() is equivalent to assertTrue(false).
The next step is to cover other possible situations:
public function testIteratesOverOneElementArrays()
    {
        $iterator = new ArrayIterator(array('foo'));
        $i = 0;
        foreach ($iterator as $element) {
            $this->assertEquals('foo', $element);
            $i++;
        }
        $this->assertEquals(1, $i);
    }
This test ensures that one-element numeric arrays are iterated correctly. The first assertion states that every element which is passed as the foreach argument is the element in the array, while the second that the foreach is executed only one time. You have probably guessed that assertEquals() confronts its two arguments with the == operator and fails if the result is false.
When it is not too computational expensive, we should strive to have the few possible assertions per method; so we can separate the test method testIteratesOverOneElementArrays() in two distinct ones:
public function testIteratesOverOneElementArraysUsingValues()
    {
        $iterator = new ArrayIterator(array('foo'));
        foreach ($iterator as $element) {
            $this->assertEquals('foo', $element);
        }
    }
    
    public function testIteratesOneTimeOverOneElementArrays()
    {
        $iterator = new ArrayIterator(array('foo'));
        $i = 0;
        foreach ($iterator as $element) {
            $i++;
        }
        $this->assertEquals(1, $i);
    }
Now the two test methods are nearly independent and can fail independently to provide information on two different broken behaviors: not using the array values and iterating more than one time on an element. This is a very simple case, but try to think of this example of a methodology to identify responsibilities of a production class: the test names should describe what features the class provides at a good level of specification (and they are really used for this purpose in Agile documentation). This is what we are doing by adopting descriptive test names and using a single assertion per test where it is possible: broken up the role of the class in tiny pieces which together give the full picture of the unit requirements.
We can go further and test also the use of ArrayIterator on associative arrays:
public function testIteratesOverAssociativeArrays()
    {
        $iterator = new ArrayIterator(array('foo' => 'bar'));
        $i = 0;
        foreach ($iterator as $key => $element) {
            $i++;
            $this->assertEquals('foo', $key);
            $this->assertEquals('bar', $element);
        }
        $this->assertEquals(1, $i);
    }
As an exercise you can try to refine this method two three independent ones, for instance creating the first of them with a name such as testIteratesOverAssociativeArraysUsingArrayKeysAsForeachKeys(). Don't worry about long method names as long as they are long to strengthen the specification, but only when the code can be refactored to smaller test methods. Even then, finding descriptive test names is the most difficult part of the process.
We can go on and add other test methods, and Spl has many.

Whenever a bug is found which you can impute to the class under test, you should add a test method which exposes the bug, and thus fails; then you can fix the class to make the test pass. This methodology helps to not reintroduce the bug in subsequent changes to the class, as a regression test is in place. It also defines more and more the behavior of a class by adding a method at the time.
The TDD methodology not only forces to add test methods to expose bug, but also to define new features. Implementing a user story is done by first writing a fail test which exposes the problem (the feature is not present at the time in the class) and then by implementing it.

I hope you're liking this journey in testing and you're considering to test extensively your code if you currently are not using phpunit or similar tools. In the next part, we will make a panoramic the assertion methods which phpunit provides to simplify the tester work. Remember that, in software unit testing, developer and tester coincide, or at least are at one next to the other, in the case of pair programming.

I have posted on pastebin the ArrayIteratorTest class if you want to play with it by yourself.
You may want to subscribe to the feed to not miss subsequent posts in this series.

No comments:

Post a Comment