Wrapping PHP Functions for Testability
-
One of the problems that hampers the testability of PHP code is the coupling created by accessing all of the PHP global functions. This happens often because a large number of useful extensions are accessed only through global functions. Consider the following code snippet:
$res = ldap_connect($host, $port); if (! $res) { // error logging return false; }
There are two code paths shown above: the connection succeeding, and it failing. Both of them are very difficult to test because of the coupling to the global function
ldap_connect()
provided by the LDAP extension.To make it succeed, you’d need an LDAP server. Causing it to fail is easier but it could take a very long time until the connection timeout occurs. Also, the code can’t be tested at all without the LDAP extension. All of these problems are unacceptable.
The solution is to use to the extension through an object instead of calling the extension function directly. This way, we can inject either the extension wrapper or a mock object for testing.
However, writing these wrappers and maintaining them can be a pain and this is often the rationale given for not using them. There’s an easy answer to this excuse:
class ExtensionProxy { protected $ext; public function __construct($ext) { $this->ext = $ext; } public function __call($method, $args) { return call_user_func_array("{$this->ext}_{$method}", $args); } }
Since most PHP extensions prefix all of their functions with the name followed by an underscore, it’s easy to wrap them with something like the class above.
There’s some performance penalty from
call_user_func_array()
in the above example but you can always write out a class later if that ever actually becomes a problem. Meanwhile, it can get you going very going quickly.Our connection example then simply becomes:
$ldap = new ExtensionProxy('ldap'); ... $res = $ldap->connect($host, $port); if (! $res) { // error logging return false; }
The difference in usage is trivial but this version is easily testable. It now depends only on an
$ldap
instance, which the class needing LDAP can receive in its constructor. To test, now just pass a mock object for$ldap
.The technique of putting lightweight wrappers around PHP extension functions has been around for a long time. For example, Horde has a small wrapper around the IMAP extension for testing.
The continued improvements in PHP 5 allow for simple tricks like the
ExtensionProxy
above, and advances in tools like PHPUnit are making tests increasingly convenient and practical.Whatever methods you choose, there really is no excuse for untested (or untestable) PHP code these days. I consider anything without good tests to be broken and you should also.