Monday, September 28, 2009

Practical testing in php part 5: annotations

This is the fifth parth of the php testing series. You may want to check out the other parts or to subscribe to the feed to be informed of new articles.

Now that we have learned much about writing tests (with or without fixtures) and using assertions, we can improve our tests further by exploiting phpunit features. This awesome testing tool provides support for several annotations which can add behavior to your test case class without making you write boilerplate code. Annotations are a standard way to add metadata to code entities, such as classes or methods, and are composed by a @ tag followed by zero, one or more arguments. While the parsing implementation is left to the tool which will use them, their aspect is consistent: phpDocumentor also collects @param and @return annotations to describe an Api.
Remember that annotations must be placed in docblock comments as in the php engine there is no native support for them: phpunit extracts them from the comment block using reflection.

While writing an Api or also a simple class, the corner cases and incorrect inputs have to be taken into consideration. The standard way to manage errors and bad situations in an oop application is to use exceptions. But how to test that a method raises an exception when needed? Of course the normal behavior is tested with real data that returns a real result. For the exceptional behavior, we can start with this test method:
    public function testMethodRaiseException()
    {
        try {
            $iterator = new ArrayIterator(42);
            $this->fail();
        } catch (InvalidArgumentException $e) {
        }
    }
The purpose of this code is to raise an exception by passing invalid data to the constructor of ArrayIterator, which requires an array. If the exception is raised accordingly, it bubbles up to the end of the try block and it is catched correctly, making the test pass. If the exception is not thrown, the call to fail() declares the test failed.
However, this paradigm will be repeated very often everytime you need to test an exception and so it can be abstracted away. Also, this code does not convey the intent of testing an exception since it is cluttered with details like an empty catch block and calls to fail().
Phpunit already abstracts away this code providing an annotation, @expectedException, which has to be put in the method docblock:
    /**
     * @expectedException InvalidArgumentException
     */
    public function testMethodRaiseExceptionAgain()
    {
        $iterator = new ArrayIterator(42);
    }
This code is much more clear than the constructs we used earlier. The only code present in the method is the one required to throw the exception, while the intent is described in the method name and in its annotations.

Another common repetition is testing a method with different kind of inputs, while executing always the same code. This is commonly resolved with a loop:
    public function testBooleanEvaluationInALoop()
    {
        $values = array(1, '1', 'on', true);
        foreach ($values as $value) {
            $actual = (bool) $value;
            $this->assertTrue($actual);
        }
    }
But phpunit can do the loop for you, taking advantage of the @dataProvider annotation:
    public static function trueValues()
    {
        return array(
            array(1),
            array('1'),
            array('on'),
            array(true)
        );
    }
    /**
     * @dataProvider trueValues
     */
    public function testBooleanEvaluation($value)
    {
        $actual = (bool) $value;
        $this->assertTrue($actual);
    }
This annotation should be followed by the name of a static method in the test case which returns an array of data sets to be passed to the test method. Phpunit will iterate over this array and using each one of its elements (which will be an array containing the arguments) to run the test method, telling you which data set was in use in case of a test failure. Of course you can put anything in the data sets: input for the SUT or expected result, or both.
The code becomes a bit longer, but the expressivity of defining the concept of different data sets in a standard way are worth considering.

The last common situation we will look at today is test dependency. Again, we are talking about dependency beneath the same test case since interdependencies between unit tests are a small of high coupling and should be raise suspects about your classes design.
It happens often that some test methods are more specific than the first you wrote and they will obviously fail if the formers do. The classic example is the add()/remove() tests on a container: to make sure remove() works you have to use add() for the arrange part of the test method. Phpunit solve this common problem of logic and temporal precedence (I won't present a workaround like in the other cases since it was not possible to solve this issue before phpunit 3.4 introduced @depends):
    public function testArrayAdditionWorks()
    {
        $array = array();
        $array[0] = 'foo';
        $this->assertTrue(isset($array[0]));
        return $array;
    }

    /**
     * @depends testArrayAdditionWorks
     */
    public function testArrayRemovalWorks($fixture)
    {
        unset($fixture[0]);
        $this->assertFalse(isset($fixture[0]));
    }
Not only testArrayAdditionWorks() is executed before testArrayRemovalWorks(), but since it returns something, this result is passed as an argument to the dependent method. If the former test fails, however, the dependent ones are marked as skipped as they will fail anyway by definition. They will clutter the output too, while it is clear that the functionality that needs repairment is the array addition.

I hope this standard phpunit annotations can help you enjoy writing tests for your php classes, leaving you the exciting work and taking off the boring one. In the next parts, we'll look at refactoring for test code before taking a journey with stubs and mocks.

I have uploaded on pastebin these code examples in a running phpunit test case. You may also want to subscribe to the feed to be informed of new posts in this series.

No comments:

ShareThis