Maintainable Unit Tests with PHPUnit
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:
- create a new factory method for each group of, e.g., Factory_Car and Factory_SuperFastCar, or
- 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.

November 5th, 2009 - 13:58
I see two issues with your examples. First, if you’re changing a constructor signature, that’s introducing a regression, plain and simple. You’re breaking a behavior and an expectation.
Second, why are you instantiating the object in each test case method? PHPUnit provides a facility for this very thing via it’s setUp() method. Most test cases I write will assign an instance of the UUT in setUp(), and then the test case methods will reference this when doing their work; you can also do this with dependencies. This provides a single location to touch per UUT when making changes.
November 5th, 2009 - 15:38
Fair point on the constructor signature. It probably is a bad example. I’ll try to come up with a new one.
Regarding using setUp() to instantiate an instance of the UUT, I think the problem I was trying to solve was avoiding constructing an instance in setUp() and then for the handful of test cases that need a different configuration re-instantiating the class. How do you handle such cases?