Practical PHP Testing Patterns: Implicit Teardown
Join the DZone community and get the full member experience.
Join For FreeIf we define the hooks supported by our test automation framework (PHPUnit for all articles in this series) will call our cleanup code. It's just a matter of placing it in the right place: almost all testing frameworks, throughout many languages, support these hooks.
The tearDown() and tearDownAfterClass() methods (the latter is static) will be executed automatically after the tests. The first, after each test method, before instantiating the next Testcase Object. As the name says, the second after all the test methods in that Testcase Class have been executed.
The contract for this hook (referenced by the Template Method PHPUnit_Framework_TestCase::run()) is that the tear down is always executed, even if the test fails F or gives an error E, or it is skipped or incomplete, and of course it succeeds.
If the framework support this pattern (and PHPUnit does) this is actually the simplest and easier to maintain solution. This pattern is the opposite of In-Line Teardown, but usually requires Automated Teardown, even in a simple form; the hook for starting up the automated process is placed in tearDown().
Implementation
When the teardown phase is equal for all tests, there is no problem in implementing the pattern.
If the teardown procedures are different, the teardown method must be smart enough to understand in which scenario it has been called. There are various tools that PHP provides you:
- isset() and unset() calls for example have the property of being language constructs (like if() and while()) and not actual functions. This means that isset($array['key']) is the shortest thing you can write which would not fail when $array does not have key as an index (Undefined index notice).
- foreach() over arrays of resources also useful, as an empty array would also be skipped.
The teardown must clean up not only the final state of different test methods, but also the different exit points of the same method. Maybe an assertion failed and the method exited early, without populating all the fixtures you want to delete. This variation (which is actually the standard way to work) is called Teardown Guard Clause.
In general, tearDown() cleans up what it finds on $this but does not blow up if there are some missing resources. It assumes they were not created in the first place.
So if you want to be sure that your resources are deleted instead of being left to rot in the test suite folders or in some memory location, put a pointer to resources in fields immediately after creation. You never know what can happen between $object = new MyClass() and $this->field = $object.
Always called? Really?
Exceptions are caught inside the run() methods, which calls your test*() one. All of them are managed: catch (Exception $e) does the job very well.
PHP errors like Notices and Warnings are treated by the error handler of PHPUNit, which simply raises exceptions, falling again in the previous case.
The only thing that can prevent a teardown from running is a Fatal Error, like calling a method on null (yes, in PHP that's a Fatal Error).
However this error will cause the process to terminate abruptly: further tests won't fail or become slower and slower because PHP will simply exit. Persistent resources like temporary files or databases however would have to be cleaned up manually.
That's why you have to ensure that your test suite never encounters a Fatal Error, even while tests are red, by placing Guard assertions where appropriate. Before calling a method on a returned value you can easily add a $this->assertInstanceOf() assertion.
It's nice to note that in PHPUnit tearDown() is called also when setUp() raises an early error.
Examples
The sample code (Github reference link) shows you hot to create a tearDown() or a teardownAfterClass() methods. It also shows how to avoid fatal errors that would prevent the teardown phase from being reached by inserting a simple Guard Assertion.
<?php
class ImplicitTeardownTest extends PHPUnit_Framework_TestCase
{
/**
* I keep the object on a field so that if In-Line Teardown is not
* executed, we'll see destruction messages at the end of the suite
* instead of after each test.
*/
private $fixtureToTeardown;
private static $classLevelFixtureToTeardown;
public function testExpectMethodForInlineTeardown()
{
$this->fixtureToTeardown = new MyClassWithDestructor(1);
self::$classLevelFixtureToTeardown = new MyClassWithDestructor(2);
$this->assertTrue(false, 'First test failure message.');
}
public function testSomethingElseWhichCouldResultInAFatalError()
{
// suppose your SUT code returns this or a scalar for a
// regression or bug
$object = null;
$this->assertInstanceOf('SplQueue', $object);
$this->assertEquals('dummy', $object->dequeue());
}
/**
* A workaround to being able to support expect() methods
*/
public function tearDown()
{
unset($this->fixtureToTeardown);
}
public static function tearDownAfterClass()
{
// you can't unset() a static property. Don't ask me why
self::$classLevelFixtureToTeardown = null;
}
}
class MyClassWithDestructor
{
private $id;
public function __construct($id)
{
$this->id = $id;
}
public function __destruct()
{
echo "The instance {$this->id} of MyClassWithDestructor has been destroyed.\n";
}
}
Opinions expressed by DZone contributors are their own.
Comments