Agile Zone is brought to you in partnership with:

I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 638 posts at DZone. You can read more from them at their website. View Full User Profile

Practical PHP Testing Patterns: Test Method

11.24.2010
| 7292 views |
  • submit to reddit

We are now starting the xUnit Basics Patterns part of this series. We will move on from the general ideas of testing strategy to more mundane things, like the organization of code in test methods and their internal structure.

The Test Method pattern is applied when you encode each test as a single Test Method on a class. Each test is standalone: it someway boostraps the fixtures it needs, like an object or a resource, then it exercises a part of it.

Implementation

A method, particularly in PHP, has a natural isolation of scope which we can't easily find with other constructs (code blocks for example do not have scope isolation in PHP.) Once we exit the method at the end of the test, the variables contained are automatically discarded.

Once we have defines our Test Methods, a test runner will use reflection to gather all the method names and run them, one at the time. PHPUnit instances also a different object of the class for each different method, to provide isolation not only for the local variables but also for the member ones.

Usually only some methods are run: the ones marked for tests. In PHPUnit, which implements the Test Method pattern as all the xUnit frameworks do, the methods starting with the word test are understood as test methods. You can define additional helper methods simply by naming them without this prefix.

If you regard tests as a low-level specification and documentation, the Test Method names should be descriptive: tests for an ArrayObject may be named testAddsAnObjectToTheCollection, testRetrievesAnObjectBasingOnOffset. The convention followed in Agile circles is to start them with as third-person singular verb, where the subject of the sentence is the System Under Test.

Variations

This pattern is specialized into different categories of Test Methods. Here are the one cited in xUnit testing patterns.

Simple Success Test

This kind of test exercises a best path, without taking into account any failures. We make assertions on the result of the called methods to check their correctness. Usually, the first test that has to be written is a Simple Success Test.

This type of tests does not comprehend catching of exceptions or error management.

Expected Exception Test

In this tests, we do something wrong on purpose, and see that an exception is raised which satisfy our expectations. Exceptions are the standard error management tool in object-oriented programming.

PHPUnit offers @expectedException annotation, along with the @expectedExceptionCode and @expectedExceptionMessage if checking the exception class or interface is not enough.

There are no happy paths exercised by these tests: each of them must provoke an error, or PHPUnit would tell you that it expected exception XXXException but it wasn't raised during all the test method run.

Since the catching is done internally by PHPUnit, you do not need to insert try/catch blocks, except for particular cases where you want to be more precise on the origin of the thrown exception. In this case, you may insert try/catch block on part of the method to check that a particular method call raises the exception, and not any other line of code. You can also call $this->setExpectedException() just before provoking the error.

Constructor test

These tests exercise a constructor, and in general the initial state of an object, separately from the Simple Success Test. In these tests, you instance an object, and then start making assertions on its initial state.

However, if the constructor makes too much work, this would make the other tests brittle as they call the constructor themselves (it can't be stubbed out.) A general practice is to insert assertions after creation for clarity (checking that a crucial field is null or 1 or equal to some other constant), but not doing actual work in the constructor, leaving the wiring code to a creational pattern such as a Factory or a Builder and freeing the other tests from the burden of depending on construction code.

Example

The code sample shows three tests, one for each variation, that exercise an ArrayObject. I always choose native classes to simplify the examples, that can simple show how testing patterns work instead of forcing the user also to learn a particular throwaway SUT.

<?php

class ArrayObjectTest extends PHPUnit_Framework_TestCase
{
    /**
     * Construction Test. It is logical to keep such tests at the start
     * of a Test Case, for clarity.
     */
    public function testIsEmptyAtConstruction()
    {
        $this->assertEquals(0, count(new ArrayObject()));
    }

    /**
     * Simple Success Test. These tests are the bread and butter of a
     * Test Case.
     */
    public function testInsertsAValueInTheCollection()
    {
        $arrayObject = new ArrayObject();
        $arrayObject['key'] = 'value';
        $this->assertEquals('value', $arrayObject->offsetGet('key'));
    }

    /**
     * Expected Exception Test. Testing error conditions is important
     * both for debugging purposes and for handling errors gracefully
     * instead of dysplaying a blank page.
     * @expectedException PHPUnit_Framework_Error_Warning
     * @expectedExceptionMessage Illegal offset type
     */
    public function testDoesNotAllowForANonScalarKeyToBeUsed()
    {
        $arrayObject = new ArrayObject();
        $key = new stdClass;
        $arrayObject[$key] = 'value';
    }

    public function testDoesNotAllowForANonScalarKeyToBeUsed_Alternative()
    {
        $arrayObject = new ArrayObject();
        $key = new stdClass;
        $this->setExpectedException('PHPUnit_Framework_Error_Warning');
        $arrayObject[$key] = 'value';
    }
}
Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)