Advanced PHPUnit: Annotations

PHPUnit, borrowing a handy feature from Java’s JUnit testing library allows you to use certain directives beginning with “@” in comments of your test or application code to access test-related functionality. These directives, known as annotations, even expose features not otherwise accessible to PHPUnit users. While references to an annotation will generally appear in the PHPUnit manual section relevant to its functionality, I could not find a comprehensive list of all available annotations in the current release and what effect they have. The following list contains (to the best of my knowledge) all of the annotations implemented as of PHPUnit 3.3, as well as some of the benefits and drawbacks of using each:


@assert

When put into a function’s docblock, this annotation causes the skeleton generator to automatically create tests with the given assertion. This can be useful for simple tests where the arguments can fit in the allowed format, especially when you want to test multiple input values since you can put multiple @assert annotations for one function. Since PHPUnit copies the argument list directly out of the annotation, you can even construct objects to supply as arguments. However, this annotation falls short when the method under test requires more complicated setup for its parameters than can fit in a single line. In general, though, using @assert and the skeleton generator can save a fair amount of time developing tests for new classes as you create them.

Example (inside application code):

    /**
     * @assert (0, 0) == 0
     */
    public function add($a, $b)
    {
        return $a + $b;
    }

Result of running the skeleton generator on the example:

    /**
     * Generated from @assert (0, 0) == 0.
     */
    public function testAdd()
    {
        $this->assertEquals(
          0,
          $this->object->add(0, 0)
        );
    }

@codeCoverageIgnoreStart & @codeCoverageIgnoreEnd

These instructions cause the code block between them to be marked as executed by code coverage reporting. This allows you to except sections of code from testing that depend upon state which should be impossible to reach in normal test situations (e.g., error handling code for failures in external systems and native PHP functions). This annotation functions slightly differently than the others, in that it will probably be used in inline comments rather than function or class PHPDoc blocks. Also, it is worth noting that the code lines will be marked in the coverage report as “covered”. This may give the false impression that the code is actually tested rather than being marked as explicitly untested.

Example (inside application code):

    public function method()
    {
        if (0)
        {
            // @codeCoverageIgnoreStart
            $this->doSomething();
            // @codeCoverageIgnoreEnd
        }
    }

The function call inside the if statement will be marked as covered in the coverage report.


@covers

This annotation causes the test to only generate code coverage information for the specified class, method, or subset of methods.

Example (inside a test class):

    /**
     * @covers SampleClass::publicMethod
     * @covers SampleClass::<!public>
     * @covers HelperClass<extended>
     */
    public function testMethod()
    {
        $result = SampleClass::method();
    }

This test method will generate coverage only for the method SampleClass::publicMethod(), all non-public methods in SampleClass, and methods in HelperClass or one of its parent classes.

The list of possible modifiers for specifying covered methods can be found in the PHPUnit manual.


@dataProvider

This indicates the given function provides an array of argument arrays to be iterated over by the test function. This comes in handy when you need to provide a series of different data sets to the function under test. In running the tests, PHPUnit will treat each argument array as a different test and provide you with the list of arguments used when the test fails, still allowing you to effectively localize failures. Using @dataProvider, however, means that you have the test code physically separated from the fixture creation code, which means you may have to scroll back and forth to figure out exactly what the data being provided to the test method represents.

Example (inside a test class):

    /**
     * @dataProvider provider
     */
    public function testAdd($a, $b, $c)
    {
        $this->assertEquals($c, $a + $b);
    }
 
    public function provider()
    {
        return array(
            array(0, 0, 0),
            array(0, 1, 1),
            array(1, 1, 3)
        );
    }

@expectedException

The @expectedException annotation, very similar to the setExpectedException() method on PHPUnit’s TestCase class, marks that the function expects a particular type of exception to be thrown. One notable difference with this syntax from setExpectedException() is that if you specify an expected message using the annotation, the parsing assumes the exception’s message contains no spaces. Thus, if you tend to throw exceptions with messages having spaces in them, the annotation will only match on the first word of the message (and will expect the second word to be the exception code). With the setExpectedException() method, the message to match against is passed in as a string and suffers no such limitations. Also, with the setExpectedException() method, you can set the expected exception immediately prior to calling the method under test, whereas with this annotation anything in the test method prior to the call to the method under test could be throwing the exception and the test would still pass.

Example (inside a test class):

    /**
     * @expectedException Exception_Test Message
     */
    public function testHelper_throwsException()
    {
        $this->helper();
    }
 
    public function helper()
    {
        throw new Exception_Test(‘Message’);
    }

@group

This marks the test method as a member of a particular test group, allowing you to easily run or exclude only a focused subset of related tests. This could be used, for example, to mark certain tests as related to a particular customer issue or to create a smaller “smoke test” when your test suite gets too unwieldy. To run multiple groups of tests simultaneously, separate the group names with commas.

Run “phpunit --group <group1,group2,...> AllTests.php” to only run tests from the given groups.
Run “phpunit --exclude-group <group1,group2,...> AllTests.php” to run all tests except those from the given groups.
Run “phpunit --list-groups AllTests.php” to get a list of all available groups in that test suite.

Example (inside a test class):

    /**
     * @group APP-5555
     * @group smokeTest
     */
    public function testMethod_takesALongTime()
    {
        $result = Class::method();
    }

@scenario

This annotation marks the test as being a scenario for Behavior-Driven Development (the explanation of which is beyond the scope of this article). If you want to know more, the PHPUnit manual contains a very informative article on BDD.


@test

This annotation is just a different syntax for marking that a given function in a test class is a test. If you put this in a function’s block comment, you do not need the function name to begin with “test…” for PHPUnit to recognize it as such. This extra freedom allows you to decide on your favorite naming scheme, such as matching the name of the function in the test class to the name in the actual class or putting “Test” at the end of the method name rather than the beginning.

Example (inside a test class):

    public function testThisIsATest()
    { ... }
 
    /**
     * @test
     */
    public function thisIsAlsoATest()
    { ... }
 
    /**
     * Just a helper function
     */
    public function thisIsNotATest()
    { ... }

New Annotations in PHPUnit 3.4:

The coming release of PHPUnit 3.4 is currently slated to bring a new set of annotations allowing access to more fun features. There is even the beginning of a comprehensive annotations list in the manual. The new release will include the following annotations (and maybe more!):

  • @backupGlobals/@backupStaticAttributes (allowing you to adjust the backing up of global variables and static class attributes per test as described in this post)
  • @depends
  • @runTestsInSeparateProcess/@runInSeparateProcess (based on this post from long ago)
  • @errorHandler (allowing you to set a test-specific error handler)
  • @outputBuffering (allowing you to wrap an output buffer around the test)
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Twitter
  • Google Bookmarks
  • DZone
  • HackerNews
  • LinkedIn
  • Reddit