Process Isolation in PHPUnit

August 19, 2010

I was recently writing a unit test for an autoloader when I came across a somewhat unintuitive behavior in PHPUnit.

One requirement of the test suite was that some test methods had to be run in a separate process since class declarations reside in the global scope and persist until the process terminates. So, I slapped a @runInSeparateProcess annotation in the docblock of a test method with that requirement, ran the test suite... and watched that test method fail because the class was still being declared.

It took some head-scratching and tracing through the source of PHPUnit itself to figure out what was going on. When you run the phpunit executable, it's actually instantiating PHPUnit_TextUI_TestRunner. The eventual result of this is that the run() method inherited by your subclass of PHPUnit_Framework_TestCase is called.

Depending on the value of the also-inherited $preserveGlobalState instance property, which can be set via the setPreserveGlobalState() method, multiple measures are undertaken to preserve the state of the current process. One such measure is including files for all the classes currently defined in that process, which is what was tripping me up because $preserveGlobalState has a default value of true. $preserveGlobalState must contain its intended value before the run() method is called. The easiest way that I've found to facilitate this is to override the run() method in your subclass, call setPreserveGlobalState() there, then call the parent class implementation of run(). I've included a code sample below to illustrate this.

class MyTestCase extends PHPUnit_Framework_TestCase
    public function run(PHPUnit_Framework_TestResult $result = NULL)
        return parent::run($result);

So, if you try to use the @runInSeparateProcess or @runTestsInSeparateProcesses annotations that PHPUnit offers, be aware that the global state will be preserved by default. You will need to explicitly set it to not be so if running tests in separate processes is to have the effect that you are probably intending.

Update: @preserveGlobalState enabled|disabled can be used in place of what I did in the PHP code itself above. Thanks to Sebastian Bergmann for pointing this out.


Matthew Weier O'Phinney says: August 20, 2010 at 8:54 am

Great analysis. I'd run into the issue myself when testing Zend\Session for ZF2, and I think that PHPUnit may have actually changed behavior at some point, as the tests ran for me in a previous version of PHPUnit I was running.

One additional note I'd add: if you have setup autoloading in your test bootstrap, the autoloaders will disappear when running tests in a separate process if you disable global state. As a result, after applying your technique, I also had to add some functionality to my tests to check for existence of the autoloader during setUp(), and, if not found, re-instate it.

Giorgio Sironi says: August 22, 2010 at 6:39 am

I encountered issues with @runTestsInSeparateProcesses while refactoring Scisr. I refactored the code to avoid using separate processes (from static classes to Dependency Injection), but unfortunately in PHP some global state cannot be avoided: autoloaders in your case, but also superglobal variables, is_uploaded_file() and so on.

Lukas says: August 23, 2010 at 4:28 am

We also ran into issues because of that. Not really sure why this is on by default. Shouldn't unit tests be independent? Moreover, when I use process isolation, I do this specifically because I want things to be isolated. Also it's kinda surprising magic. Anyway, once you know how to turn it off, all is as well.