Jeff Carouth's blog Ramblings of a Web Application Developer

5Nov/09Off

Maintainable Unit Tests with PHPUnit

Better version of this post coming. Thanks to Matthew Weier O'Phinney's comment I see that this post did not convey the point in the manner I wanted, and I thought of a better solution to the problem. Thus I am closing comments on this post pending the new version. Read the modified version.

Great unit testing is a learned skill. Unfortunately the motivation developers have for dabbling in writing unit tests for an application usually comes from someone else, e.g., his or her boss, a fellow team member, etc. This leads to writing tests just to have tests which, in my opinion at least, is no better than not having tests in the first place.

A non-maintainable code base is know to be a problem. The corollary, a non-maintainable test suite is a huge problem, is also true. If you cannot maintain your test suite, what is the point?

Looking at an example, I frequently see test cases that have the UUT's constructor called at the beginning of each test. This works fine up until the requirements change and the constructor now requires an instance of IDataSource or an array instead of a string. Now each test must be touched, i.e., development time devoted to each test function, to implement this change.

class CarTest extends PHPUnit_Framework_TestCase
{
    public function testStartFailsIfGasTankEmpty()
    {
        $car = new Car();
        $this->setExpectedException('GasTankEmptyException');
        $car->start();
    }

    public function testSomethingElse()
    {
        $car = new Car();
        //assert something
    }

    //so on and so forth
}

Instead, if a developer uses a factory method within the test suite to return a default instance of the UUT, the change to the tests is only necessary in cases where the initialized state of the instance must change via the constructor, as follows.

class CarTest extends PHPUnit_Framework_TestCase
{
    public function testStartFailsIfGasTankEmpty()
    {
        $car = $this->Factory_Car();
        //…snip…
    }

    //…snip…

    private function Factory_Car()
    {
        return new Car();
    }
}

Okay, that works for a while. But what if in ninety-five percent of the tests you need to use the "default constructor"—in quotes because PHP classes can only have one constructor—but the other five percent need to pass in some configuration options. You have two choices:

  1. create a new factory method for each group of, e.g., Factory_Car and Factory_SuperFastCar, or
  2. allow the existing factory method to take arguments and pass them on to the constructor.

Which you choose depends on the unique situation of your test case. For this example I'll assume there are ten tests. Seven of the tests use a default Car object while the other three each need a unique Car instance, thus each passes a configuration argument to the constructor. To handle this situation the factory method will make use of the PHP functions func_get_args() and call_user_func_array().

public function Factory_Car()
{

    $car = new Car();
    if (func_num_args() > 0) {
        $args = func_get_args();
        call_user_func_array(array($car, '__construct'), $args);
    }

    return $car;
}

This factory method allows each test to retrieve a properly-configured instance—where configuration is done via the constructor—of the Car class with which to work without sacrificing maintainability. One caveat: the three test methods in the example will need to be modified if the constructor's signature changes, but, last time I checked, three is less than ten.

by

Recent Comments

Categories

Recently Authored

Archives

Blogroll