Practical PHP Testing Patterns: Testcase Object
Join the DZone community and get the full member experience.
Join For FreeThe Testcase Object pattern, which goes hand in hand with the Testcase Class one, is a specialized Command pattern, applied to the single tests of your suite.
The goal of the Testcase Object is to decouple the Testcase methods definition from the actual tests executed, for example recognizing a @dataProvider annotation as we saw in the related article.
Implementation
Here's how this pattern works in conjunction with Testcase Class:
- first, the runner someway gathers a list of which classes should be run; how it acquires this list is out of the scope of this pattern, but it's usually simply by selecting all the classes in a particular folder whose name ends with Test (at least by default).
- Then, it uses the PHP reflection Api to find out which methods are present on those classes. Again, by default (and I don't think it's customizable, at least in PHPUnit) a naming convention is used: the public methods starting with test.
- An object for each Test Method is instantiated. As explained in the Testcase class article, this is way the whole class is called Testcase: because each instance correspondes to a single Test Method run. In some cases, like in the presence of @dataProvider, multiple Testcase Objects are created for a method.
- The set of Command objects is now ready, and it is run in sequence. Then the results are ready for displaying or gathering into an XML (or some other fancy format) report.
Part of this logic is in the Test Runner, part of it in the base Testcase Class itself for the sake of encapsulation. For instance, the runner only calls a Template Method run() over the Testcase Object, while this method handles internally setUp() and tearDown().
Trade-offs
When there are many test methods in a single Testcase Class, performance can be altered by this one-object-per-test behavior, so there is a trade-off to make. When you are sure that, by design, different assertions do not influence the System Under Test, for example because it does not have a state, you are free to use multiple assertions in the same test. The goal is to avoid instantiation of a large number of Testcase Objects and respective SUTs, especially in the case the Test Methods are too many. There are other solutions however to reuse the same SUT|Shared Fixture link.
An alternative is to prepare the fixture in the setUpBeforeClass() and tearDownAfterClass() static methods, but this will leave the objects created available only as static fields, which may not suite your taste (at least make them private).
Example
In the code sample, we will see a glimpse of PHPUnit's internals, since the Testcase Object creation is something which is taken care for you and don't have to deal with. Here's a part of the base Testcase Class your Testcase Objects will always be instance of.
<?php
abstract class PHPUnit_Framework_TestCase extends PHPUnit_Framework_Assert implements PHPUnit_Framework_Test, PHPUnit_Framework_SelfDescribing
{
/**
* Constructs a test case with the given name.
*
* @param string $name this is the method to call
* @param array $data this are parameters for the test method, in case of @dataProvider annotation
* @param string $dataName
*/
public function __construct($name = NULL, array $data = array(), $dataName = '')
{
if ($name !== NULL) {
$this->setName($name);
}
$this->data = $data;
$this->dataName = $dataName;
}
/**
* Runs the test case and collects the results in a TestResult object.
* If no TestResult object is passed a new one will be created.
*
* @param PHPUnit_Framework_TestResult $result
* @return PHPUnit_Framework_TestResult
* @throws InvalidArgumentException
*/
public function run(PHPUnit_Framework_TestResult $result = NULL)
{
if ($result === NULL) {
$result = $this->createResult();
}
// ...
$this->result = $result;
// ...
// ...
$result->run($this);
// ...
if ($this->useErrorHandler !== NULL) {
$result->convertErrorsToExceptions($oldErrorHandlerSetting);
}
$this->result = NULL;
return $result;
}
/**
* Verifies the mock object expectations. This is called automatically at the end of a test.
*
* @since Method available since Release 3.5.0
*/
protected function verifyMockObjects()
{
foreach ($this->mockObjects as $mockObject) {
$this->numAssertions++;
$mockObject->__phpunit_verify();
$mockObject->__phpunit_cleanup();
}
$this->mockObjects = array();
}
/**
* Returns a mock object for the specified class.
* This is one of the API methods that your Testcase Class inherits. In this case, it is the entry point for the mocking framework embedded in PHPUnit.
*
* @param string $originalClassName
* @param array $methods
* @param array $arguments
* @param string $mockClassName
* @param boolean $callOriginalConstructor
* @param boolean $callOriginalClone
* @param boolean $callAutoload
* @return PHPUnit_Framework_MockObject_MockObject
* @throws InvalidArgumentException
* @since Method available since Release 3.0.0
*/
public function getMock($originalClassName, $methods = array(), array $arguments = array(), $mockClassName = '', $callOriginalConstructor = TRUE, $callOriginalClone = TRUE, $callAutoload = TRUE)
{
$mockObject = PHPUnit_Framework_MockObject_Generator::getMock(
$originalClassName,
$methods,
$arguments,
$mockClassName,
$callOriginalConstructor,
$callOriginalClone,
$callAutoload
);
$this->mockObjects[] = $mockObject;
return $mockObject;
}
/**
* Returns a builder object to create mock objects using a fluent interface.
*
* @param string $className
* @return PHPUnit_Framework_MockObject_MockBuilder
* @since Method available since Release 3.5.0
*/
public function getMockBuilder($className)
{
return new PHPUnit_Framework_MockObject_MockBuilder(
$this, $className
);
}
/**
* Returns a matcher that matches when the method it is evaluated for
* is executed zero or more times.
* Another API method: this time it serves to provide a matcher for the construction of expectations on mock objects. There are many methods akin to this, like for example once() or never().
*
* @return PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount
* @since Method available since Release 3.0.0
*/
public static function any()
{
return new PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount;
}
/**
* Hook methods which are by default empty, but can be redefined by your Testcase Class.
* This method is called before the first test of this test class is run.
*
* @since Method available since Release 3.4.0
*/
public static function setUpBeforeClass()
{
}
/**
* Sets up the fixture, for example, open a network connection.
* This method is called before a test is executed.
*
*/
protected function setUp()
{
}
/**
* Tears down the fixture, for example, close a network connection.
* This method is called after a test is executed.
*/
protected function tearDown()
{
}
/**
* This method is called after the last test of this test class is run.
*
* @since Method available since Release 3.4.0
*/
public static function tearDownAfterClass()
{
}
}
Opinions expressed by DZone contributors are their own.
Comments