Fig 1. Some kind of horrific monkey-muppet-tellytubby hybrid. |
I'm talking, of course, about mocking internal PHP functions and methods, so that we can test code that relies upon them.
I work on a vast PHP code base, it is 3M LOC of PHP alone. It's somewhere between legacy and modern, work is ongoing. I imagine this is the position lots of us find ourselves in, it's certainly not the first time I found myself here.
When I joined the current project there were many many tests, they relied upon the kind of unholy magic that runkit allows you to perform, for the most part this worked okay for a while. However, runkit inexplicably caused many of the tests to fault, either at shutdown, or at random.
I'm a very vocal, even forceful advocate of running the latest and greatest versions of PHP, as a result of that, we wanted to get to testing with OpCache. We can't very well judge whether optimizations performed by opcache are causing our considerably complicated, and legacy code to fold under lab conditions if we cannot get to the end of a test suite without crashes.
So we were in a bit of a jam, I've always found runkit to be quite awkward, and now I'm staring its source code in the face knowing it represents a road block to my goal of running the latest stable versions of PHP, with the first decent optimizer that ever existed for Zend.
I tackled the problem with code, code which I was allowed by my gracious employer to open source.
Tackling the problem with code might have been the wrong thing, but we got a cool extension out of it so read on, whatever your thoughts on that.
The Root Problem
This is likely a problem we have all come across; while writing unit tests we have to dodge code that invokes internal functionality, such as fopen or file_get_contents, or we have to, in some sense, write our code in mind of testability.
We find ourselves wrapping internal functionality in what we playfully (read:wrongly) refer to as "utility classes", so that even our production code has the overhead associated with what we require at test time. This is really a quite horrible solution in 100 LOC, in 3M LOC it isn't a solution at all.
In the same way, using namespaces to avoid the problem is a gargantuan task in a legacy application that is of any size, and is a fragile solution whatever, in my opinion.
We find ourselves wrapping internal functionality in what we playfully (read:wrongly) refer to as "utility classes", so that even our production code has the overhead associated with what we require at test time. This is really a quite horrible solution in 100 LOC, in 3M LOC it isn't a solution at all.
In the same way, using namespaces to avoid the problem is a gargantuan task in a legacy application that is of any size, and is a fragile solution whatever, in my opinion.
The Obvious Solution
runkit allows you to redefine internal functions or methods, but it does it with strings that contain code, in a pretty awkward way.As mentioned, I had hit a wall in working out what was causing runkit to fail in our particular case, however did realize that we could do-over the bits of runkit that make sense in a more modern way.
The Modern Solution
Here is a test case, that although requires the uopz extension, is a self-contained test case that invokes a built-in PHP function.
/* composer.json: { "require": { "phpunit/phpunit": "4.6.*@dev" } } */ require_once("vendor/autoload.php"); class Test extends PHPUnit_Framework_TestCase { public static function setupBeforeClass() { $fopen = uopz_copy("fopen"); uopz_function("fopen", function($file, $mode) use ($fopen) { switch ($file) { case "test.stream": return STDOUT; default: return $fopen($file, $mode); } }); } public function testOpenTestStreamIsResource() { $this->assertInternalType( 'resource', fopen("test.stream", "w")); } public function testOpenTestStreamIsSTDOUT() { $this->assertEquals( fopen("test.stream", "w"), STDOUT); } public function testOpenOtherStreamIsNotSTDOUT() { $this->assertNotEquals( fopen("php://temp", "w"), STDOUT); } public static function tearDownAfterClass() { uopz_restore("fopen"); } }
Hopefully, it is obvious from the example that you can manipulate internal functions and methods in a much more fluid way than runkit allowed. The same kind of manipulations can be performed on user code.
In addition to the useful bits of runkit, uopz reimplements the useful bits of Sebastian's test_helpers extension, and a rather cool runtime class composition function.
Sebastian has expressed the desire to deprecate test_helpers in favour of uopz.
I'm not going to bore the reader with a bunch of example code, head over to the php manual to read more about what uopz can do.