Practical PHP Testing Patterns: Prebuilt Fixture
Join the DZone community and get the full member experience.
Join For FreeThe goal we want to tackle today is: how to prepare Shared Fixture before the first test runs? There are of course other approaches, like lazy creation, but simply having it always available simplifies the rest of the test suite. For instance, the tests can simply assume the database connection is there.
Another goal of this pattern, Prebuilt Fixture, is reducing overhead; you don't need to open a new database connection for every single test, as you can reuse the same one if you are careful in resetting its state.
Resetting the state of a shared database is covered by other patterns, but how to create that connection for the first time is a simple example of this one.
An important point to keep in mind is not depending on manual creation of the fixture to run your tests: for example, a local database with the updated schema. If you go that way, you will start running your tests less often just to avoid the pain of having to recreate or update that database. Even with Prebuilt Fixtures, you should still be able to run any subset of your test suite at the push of a button.
A last caveat, inherited from Shared Fixtures, is that an immutable fixture is easier to share.
Basically this pattern implements Shared Fixtures which are always built, while the previous one, Implicit Setup, always creates Fresh Fixtures for each test. The next one, Lazy Setup, will deal with fixtures which are not always built, but only when needed.
Implementation
PHPUNit's --bootstrap argument or boostrap attribute in XML configuration is all the fancy technology we need.
Once upon a time, you had to scatter require_once() statements into your test case files because if you ran a test in isolation, it would still have to call the boostrap, for example to setup autoloading.
Now PHPUnit provides the boostrap hook so that every group of tests, from 1 to all the tests in the suite, can be executed after a boostrap file is included. Even when filtering a single test from a Testcase Class, you will still run automatically the bootstrap, once and for all until the phpunit command exits.
In case of framework applications, you'll probable have some code to reuse in the included file. For Zend Framework, instancing a Zend_Application object is the way to go: you can then bootstrap only the resources you want to share between tests, or the whole application for the first time.
Examples
The sample code shows you how a boostrap file looks like and how it can create a Shared Fixture that all tests can use. The only command you need to run the tests is:phpunit SampleTest.phpor, to execute all the concrete tests (here there is only one):
phpunit
Here's the PHP test code, plus the configuration file. I could have created a small repository on Github, but I think setting up by yourself teaches a lot more than doing a git pull. You can just create these files in an empty directory.
bootstrap.php
<?php
// let's suppose this connection is an Oracle or MySQL one: we want to share it
// between tests
// For SQLite connections, they may have many tables in them, and we do not
// want to recreate all the schema in a new (Fresh Fixture) connection.
$db = new PDO('sqlite::memory:');
require_once 'BaseTest.php';
BaseTest::setDb($db);
// without this, PHPUnit will try to serialize $db in order to backup all global
// variables between tests
unset($db);
phpunit.xml
<!-- defines bootstrap.php as a file to require_once() prior to executing any test suite -->
<phpunit bootstrap="bootstrap.php">
<testsuite>
<directory>./</directory>
</testsuite>
</phpunit>
BaseTest.php
<?php
abstract class BaseTest extends PHPUnit_Framework_TestCase
{
/**
* @var PDO
*/
private static $db;
/**
* We could even check this is called only once, but it could
* be necessary to call it more times in order to reset the connection
* in some corner cases.
*/
public static function setDb(PDO $db)
{
self::$db = $db;
}
/**
* Return the statically-set connection.
* If there is a configuration error and the connection was not initialized,
* we throw immediately an exception to avoid fatal errors, like someone calling
* $db->exec() on null.
*/
protected function getDb()
{
if (self::$db === null) {
throw new Exception('Db was not initialized.');
}
return self::$db;
}
}
SampleTest.php
<?php
class SampleTest extends BaseTest
{
public function testSomeQuery()
{
// the connection has been already initialized
$db = $this->getDb();
$this->assertTrue($db instanceof PDO);
//...
}
}
Opinions expressed by DZone contributors are their own.
Comments