Practical PHP Testing Patterns: Lazy Setup
Join the DZone community and get the full member experience.
Join For FreeShared Fixtures are useful to save time, and speed up execution: a resource is shared between many tests and is not recreated from scratch everytime. The typical example is always a database connection, which sometimes has an heavy bootstrap.
Sometimes however Shared Fixtures are not strictly needed in all the test suite: if I want to run a single test in memory, or even a large group of in-memory unit tests, I don't need a MySQL database connection to be created. I want this connection to be established only if I run a database-related test.
Here using the --bootstrap hook falls short as what it creates can go unused in any of the test currently run. When phpunit exits because the tests are finished, the fixture is thrown away and creating it has only been a waste.
And here comes a new pattern for fixture creation - the application of lazy creation to this problem: Lazy Setup.
When and why should I use a Lazy Setup?
You can use a Lazy Setup instead of Prebuilt Fixture when you do not want to always create the Shared Fixture. this is the main use case. If you select only a subset of the suite for running, the heavy fixture won't get created.
You can also use a Lazy Setup instead of Delegated Setup when you simply want to reuse a fixture (making it Shared instead of Fresh) but you do not want to always force a creation.
Note that Lazy Setup makes sense only for Shared Fixtures: Fresh Fixtures by definition are created everytime, and this is not viable if they're expensive in time and resources like a new database with a predefined schema.
An important issue with this pattern is: do not over use it. Almost always a Lazy Setup imply some global state left over between tests. This global state is maintained as a trade-off with speed: when speed is not an issue, or performance improvements of the test suite are not noticeable, starting from a clean slate in each test is preferrable. It's like using explosives to build a tunnel: it saves a lot of time, but it can be disruptive and can cause the whole structure to break and fall down.
Implementation
A Lazy Setup is commonly represented as a method to call, which will give you back your lazy created resource. This method may be:
- shared in the Testcase Class (called by different Test Methods of a PHPUnit_Framework_TestCase)
- shared between different Testcase Classes (with subclassing, to keep a little encapsulation).
That method may be called:
- as a standalone method (only some Test Methods use it)
- in the setUp() (only some Testcase Classes use it, but all their methods).
Example
The code sample shows how to share a PDO sqlite memory, which is usually already very fast. This is a real world case: we started sharing the schema between tests because creating it every time (80 tables) was very slow: Doctrine must create the schema in one shot and if you recreate 80 tables for hundreds of different tests, you're going to waste a lot of time.
<?php
/**
* Each class that use the shared in-memory database should extend this one.
* By providing subclassing, we at least encapsulate this static field.
*/
abstract class BaseDatabaseTest extends PHPUnit_Framework_TestCase
{
/**
* @var PDO
*/
private static $db;
/**
* Returns always the same instance. A static field maintains the
* reference throughout different test cases: use this pattern with case
* because it involves a mutable Shared Fixture. Most of the times
* performance is already good and you don't need it; databases however are
* much slower than memory and database connections and *schemas* should
* be shared.
* @return PDO a connection to a database with already created schema
*/
public function getDb()
{
if (self::$db === null) {
self::$db = new PDO('sqlite::memory:');
self::$db->exec('CREATE my_table (id INT PRIMARY KEY, name VARCHAR(255))');
}
return self::$db;
}
/**
* Deletes all rows from the connection Shared Fixture.
*/
public function tearDown()
{
if (self::$db !== null) {
$this->getDb()->exec('DELETE FROM my_table');
}
}
}
/**
* One of the many tests needing a database to work.
*/
class ADatabaseTest extends BaseDatabaseTest
{
public function testThatNeedsAConnection()
{
$db = $this->getDb();
$this->assertTrue($db instanceof PDO);
}
public function testItsAlwaysTheSameConnectionAndDatabase()
{
$db = $this->getDb();
$this->assertSame($db, $this->getDb());
}
}
Opinions expressed by DZone contributors are their own.
Comments