Mock functions in your unit tests

Complete unit test coverage says nothing about your code. But once you're actually trying to cover all your code paths, it can be quite frustrating that you can't cover everything, because you depend on the result of a built-in PHP function. In this blogpost, I will describe a way (call it a hack or dirty code, I don't care) to mock those built-in PHP functions so you can achieve complete coverage. This one is for the 100%-junkies!

A quick example

Here's a small example of what I encountered while working on a side project:

protected function getCliOutput($command) { $descriptorspec = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'a') ); $process = proc_open($command, $descriptorspec, $pipes); if (!is_resource($process)) { throw new RuntimeException( 'Could not open a resource to the exiftool binary' ); } $result = stream_get_contents($pipes[1]); fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); return $result; }

As you can see, there is a certain branch of the possible code paths which relies on the failure of receiving a resource from the proc_open function.

Since I wasn't able to control what's returned from the proc_open function, that part of the code was never covered by a unit test.

Hello namespaces

Thanks to a, let's call it creative, usage of namespaces, controlling the result from proc_open now is possible.

In short, the method can be described as "Re-implement the desired function in a separate namespace and then call the original method when necessary".


*/ class ExiftoolProcOpenTest extends \PHPUnit_Framework_TestCase { // ... } }

The important parts here are:

  • Use the same function definition
  • Create it in the same namespace as the System Under Test
  • In the SUT, don't use the function from the root namespace (proc_open(...) and not \proc_open(...)), or this method will fail.

This allows you to create a unit test which tests that if-branch.

Wait, there's more!

This is all fine and dandy, but the hard-coded return false; now causes all functionality (and tests) to break. We need to be able to control when to return false;.

To achieve this, we need to play around with namespaces even more:

*/ class ExiftoolProcOpenTest extends \PHPUnit_Framework_TestCase { // ... } }

Since we cannot alter the function signature, we can't add a variable to indicate that we want to receive a false or a real result. We need to rely on a global variable. (Yikes!) We define that global variable in the root namespace, and initialize it to false. Then in our stub-function, we import that variable through the global keyword. If we set that variable to false, we return an actual result from the original function. When that variable is set to true, we return our other result.

Now to use this in a unit test:

/** * @covers \PHPExif\Adapter\Exiftool:: */ class ExiftoolProcOpenTest extends \PHPUnit_Framework_TestCase { /** * @var \PHPExif\Adapter\Exiftool */ protected $adapter; public function setUp() { global $mockProcOpen; $mockProcOpen = true; $this->adapter = new \PHPExif\Adapter\Exiftool(); } public function tearDown() { global $mockProcOpen; $mockProcOpen = false; } /** * @group exiftool * @covers \PHPExif\Adapter\Exiftool::getCliOutput * @expectedException RuntimeException */ public function testGetCliOutput() { $reflMethod = new \ReflectionMethod('\PHPExif\Adapter\Exiftool', 'getCliOutput'); $reflMethod->setAccessible(true); $result = $reflMethod->invoke( $this->adapter, sprintf( '%1$s', 'pwd' ) ); } }

Import the global variable $mockProcOpen in your unit test method (here I used the setUp functionality), and then set it to the desired value.

This now enables you to cover that if-branch, and achieve 100% coverage.

In conclusion

Is it possible to now mock/stub built-in PHP functions? Yes. Is it dirty? Yes. But hey, no-one ever said you cannot write dirty code once in a while to achieve 100% coverage, right? ;-)

In all seriousness, only when that 100% coverage is really important to you, you should resort to such measures. Thanks to namespaces, it now is possible to do this. Whether you actually want to use this, is up to you.


Op zoek naar innovatieve manieren om te groeien?

Neem contact op