ext/test_helpers
ext/test_helpers
is an extension for the PHP Interpreter to ease testing of PHP code.
Installation
ext/test_helpers
should be installed using the PEAR Installer. This installer is the backbone of PEAR and PECL, which provides a distribution system for PHP packages and extensions, and is shipped with every release of PHP since version 4.3.0.
The PEAR channel (pear.phpunit.de
) that is used to distribute ext/test_helpers
needs to be registered with the local PEAR environment:
sb@ubuntu ~ % pear channel-discover pear.phpunit.de
Adding Channel "pear.phpunit.de" succeeded
Discovery of channel "pear.phpunit.de" succeeded
This has to be done only once. Now the PEAR Installer can be used to install extensions and packages from the PHPUnit channel:
sb@ubuntu ~ % pecl install phpunit/test_helpers
downloading test_helpers-1.0.0.tgz ...
Starting to download test_helpers-1.0.0.tgz (6,980 bytes)
.....done: 6,980 bytes
4 source files, building
.
.
.
install ok: channel://pear.phpunit.de/test_helpers-1.0.0
You should add "extension=test_helpers.so" to php.ini
Further information about building stand-alone extensions for PHP can be found in the Installation of PECL extensions section of the PHP manual.
Usage
Intercepting the Exit Statement
When a unit test exercises code that contains an exit
/ die
statement, the execution of the whole test suite is aborted. This is not a good thing.
With the set_exit_overload()
function it is possible to overload the exit
/ die
statement and make it a no-op, for instance:
<?php
set_exit_overload(function() { return FALSE; }
exit;
print 'We did not exit.';
unset_exit_overload();
exit;
print 'We exited and this will not be printed.';
?>
The code above will output
We did not exit.
The callback registered by set_exit_overload()
might receive a parameter in case exit
/ die
was called with a parameter:
<?php
set_exit_overload(function($param = NULL) { echo ($param ?: "No value given"), "\n"; return FALSE; }
die("Hello");
die;
?>
The code above will output
Hello
No value given
Another way of dealing with low-level functions and statements such as die()
and exit
is to wrap them in a proxy that by default (in production) delegates to the native implementation but for testing has a "testable" behaviour.
Intercepting Object Creation
In a unit test, mock objects can simulate the behavior of complex, real (non-mock) objects and are therefore useful when a real object is difficult or impossible to incorporate into a unit test.
A mock object can be used anywhere in the program where the program expects an object of the mocked class. However, this only works as long as the object can be passed into the context where the original object is used.
Consider the following example:
<?php
class SomeClass
{
public function doSomething()
{
$object = new SomeOtherClass;
// ...
}
}
?>
With the code above, it is impossible to run a unit test for the SomeClass::doSomething()
method without also creating an object of SomeOtherClass
. As the method creates the object of SomeOtherClass
itself, we cannot inject a mock object in its stead.
In a perfect world, code such as the above could be refactored using Dependency Injection:
<?php
class SomeClass
{
protected $object;
public function __construct(SomeOtherClass $object)
{
$this->object = $object;
}
public function doSomething()
{
// ...
}
}
?>
Unfortunately, this is not always possible (not because of technical reasons, though).
This is where the set_new_overload()
function comes into play. It can be used to register a callback that is automatically invoked when the new
operator is executed:
<?php
class Foo {}
class Bar {}
function callback($className) {
if ($className == 'Foo') {
$className = 'Bar';
}
return $className;
}
var_dump(get_class(new Foo));
set_new_overload('callback');
var_dump(get_class(new Foo));
?>
string(3) "Foo"
string(3) "Bar"
The new
operator callback can be unset when it is no longer required:
<?php
class Foo {}
class Bar {}
function callback($className) {
return 'Bar';
}
set_new_overload('callback');
var_dump(get_class(new Foo));
unset_new_overload();
var_dump(get_class(new Foo));
?>
string(3) "Bar"
string(3) "Foo"
Class Posing
The set_new_overload()
function can be used to implement a programming language feature named Class Posing. The implementation of Class Posing in Objective-C, for instance, permits a class to wholly replace another class within a program. The replacing class is said to "pose as" the target class.
Class Posing has the following restrictions
- A class may only pose as one of its direct or indirect superclasses
- The posing class must not define any new instance variables which are absent from the target class (though it may define or override methods).
- The target class may not have received any messages prior to the posing.
These restrictions are not enforced by ext/test_helpers
because the extension is only intended to ease the development of unit tests (for legacy software systems that cannot be refactored to use Dependency Injection).
Renaming Functions
The rename_function()
function can be used to rename function:
<?php
function foo()
{
// ...
}
function foo_stub()
{
return 'stubbed result';
}
rename_function('foo', 'foo_orig');
rename_function('foo_stub', 'foo');
var_dump(foo());
rename_function('foo', 'foo_stub');
rename_function('foo_orig', 'foo');
?>
string(14) "stubbed result"
This allows the stubbing/mocking of functions.
Notes
If this extension is used in combination with other extensions, such as Xdebug, which are also overloading the ZEND_NEW
opcode you have to load it as zend_extension
after loading the conflicting extension. This can be done in your php.ini
like this:
zend_extension=xdebug.so
zend_extension=test-helpers.so
Please refer to phpinfo()
to verify whether a conflict was detected and whether the work-around was enabled.