Practical PHP Testing Patterns: Configurable Test Double
Join the DZone community and get the full member experience.
Join For FreeA Test Double is instantiated from a class, that can be reused by instancing different objects from it. This class is a child of an already existing class, or an alternate implementation of a known interface.
Thus some versions of the Test Double pattern (and of its specializations Stub, Mock, Spy and Fake) are called Configurable, since they share a common class whose objects are configured in each test.
A single Test Double object is fed the values to return, or the behavior to mimic, or the calls to expect and verify. This pattern is complemented to the Hard-Coded Test Double one, where a Test Double class is very specific and cannot be reused.
Implementation
The class source code for a Configurable Test Double can be generated or coded by hand, as this is orthogonal to the configurability/hard-coding choice.
In the case of coding by hand, you'll have to include some instance variables in the class.
Configuration can be performed via the constructor or additional methods not included in the original contract of the Test Double (and thus that are called only by the test code and not by the System Under Test, which does not know them.) This is called a separate Configuration Interface.
In this solution, you implement the methods you need, by hand. Some can be left empty if really not needed, but Configurable Test Doubles tend to be complete implementations where each method can return a value (in order for the reuse to span the whole test suite).
In the case of generation via PHPUnit, you get the Test Double as always via getMock() or getMockBuilder() and similar methods. You then configure it by calling expect(), which returns an expectation object where further details can be specified (like parameters, return values, method name, and so on).
Variations
Some additions can spice up this pattern.
- Configuration Interface: a separate set of methods that can be used to configure the Test Double. The SUT does not know nor depend on them, and they are called in the test code only.
- Configuration Mode: a record and playback interface where you call the methods on a Test Double in its initial state, or on an object linked to the Test Double, in exactly the same way you expect them to be called by the SUT.
While there is a fundamental choice of implementation on coding versus generation.
- Hand-Built Test Double: Test Double coded by hand, and maintained as such.
- Statically/Dynamically Generated Test Double: the other way around; Test Double whose code is generated by PHPUnit or another mocking framework. With PHPUnit it is always Dynamically Generated (at runtime instead of as a previous step), and I think it is true for all PHP implementations.
Example
In the code sample, we expand the Test Stub article's code and play with different variations of this pattern, and different Api to perform the configuration.
<?php
/**
* We expand the example of the Test Stub article in order to discuss
* configuration more than stubbing.
*/
class ConfigurableTestDoubleTest extends PHPUnit_Framework_TestCase
{
/**
* This Test Double is hand rolled and configurable: the constructor
* is the way chosen to perform configuration.
*/
public function testCalculatesAverageVisitorsNumber()
{
$source = new StubDataSource(array(40000, 50000, 100000, 20000, 40000));
$statistics = new Statistics($source);
$this->assertEquals(50000, $statistics->getAverage());
}
/**
* This Configurable Test Double use a separate interface (addSample())
* for the sake of configuration. This interface is oriented to ease of
* testing more than to the design of the production code.
*/
public function testCalculatesAverageVisitorsNumberFromAnotherTestDouble()
{
$source = new EasilyConfigurableStubDataSource;
$source->addSample(40000)->addSample(50000)->addSample(60000);
$statistics = new Statistics($source);
$this->assertEquals(50000, $statistics->getAverage());
}
/**
* You can easily generate Stub with PHPUnit. This time, the configuration
* is done via the expects() method, which return a configuration object.
*/
public function testCalculatesAverageVisitorsNumberWithGeneratedStub()
{
$source = $this->getMock('DataSource');
$source->expects($this->any())
->method('getSamples')
->will($this->returnValue((array(40000, 50000, 100000, 20000, 40000))));
$statistics = new Statistics($source);
$this->assertEquals(50000, $statistics->getAverage());
}
/**
* When you don't like PHPUnit's interface, or it is a too low level
* of abstraction, you can always wrap it and build something more friendly.
*/
public function testCalculatesAverageVisitorsNumberWithGeneratedStubAndAFriendlyInterface()
{
$source = $this->getMock('DataSource');
$this->addSamples($source, array(40000, 50000, 100000, 20000, 40000));
$statistics = new Statistics($source);
$this->assertEquals(50000, $statistics->getAverage());
}
private function addSamples($mock, array $samples)
{
$mock->expects($this->any())
->method('getSamples')
->will($this->returnValue($samples));
}
}
/**
* The System Under Test (same as previous article).
* It requires a DataSource collaborator to be used in production,
* or to be tested.
*/
class Statistics
{
private $source;
public function __construct(DataSource $source)
{
$this->source = $source;
}
public function getAverage()
{
$samples = $this->source->getSamples();
return array_sum($samples) / count($samples);
}
}
/**
* This is the contract of the source, the collaborator for the SUT.
* It's not mandatory to have an explicit interface, particularly in PHP,
* but it helps.
*/
interface DataSource
{
/**
* @return array numerical values of visitors to this website
*/
public function getSamples();
}
/**
* An Configurable Test Double configured via constructor.
*/
class StubDataSource implements DataSource
{
private $samples;
public function __construct(array $samples)
{
$this->samples = $samples;
}
public function getSamples()
{
return $this->samples;
}
}
/**
* Another Configurable Test Double, with a separate Configuration Interface.
*/
class EasilyConfigurableStubDataSource implements DataSource
{
private $samples = array();
/**
* @return EasilyConfigurableStubDataSource
*/
public function addSample($sample)
{
$this->samples[] = $sample;
return $this;
}
public function getSamples()
{
return $this->samples;
}
}
Opinions expressed by DZone contributors are their own.
Comments