PHPUnit Mock Hard Dependencies with Aliases

With PHPUnit I need to mock a class that is declared deep, deep in an arrangement of other classes. I’ll show you how easy it is with class_alias() and the right PHPUnit annotations.

Background

A given class spins up a headless Chrome instance. I don’t want to fire up and close a real Chrome instance for each test in a given test suite (I’m testing a queue manager, if you are wondering). If I run just this test suite alone – say it’s called ChromeProcessManagerTest – then I can use class_alias() above all my other use declarations to alias the original namespace to the mocked namespace. In this case, I want to alias

Draken\ChromePHP\Core\ChromeProcess

to

DrakenTest\ChromePHP\Mocks\ChromeProcessMock

However, and this is a big however, the mock class will be used for all tests in all test suites after this test suite, or if the original namespace has been declared already, say in a previous test suite, then class_alias() will throw are warning and fail.

Once a namespace alias has been set with class_alias(), it cannot be unset.

Alias Namespaces Per Test Suite with @runTestsInSeparateProcesses

No matter how creative you are, you cannot undo a class_alias() in the same running process. The answer to the problem is right there in that very statement: “in the same running process“.

PHPUnit1 has the ability to run tests in separate processes. At the beginning of my test suite, ChromeProcessManagerTest, I can add the aptly-named declaration @runTestsInSeparateProcesses to do exactly this and run each test in a separate PHP process, each happily isolated from all the other tests and test suites, and free to class_alias() without restraint.

However, there is a footnote in the PHPUnit docs that is worth repeating:

Note: By default, PHPUnit will attempt to preserve the global state from the parent process by serializing all globals in the parent process and unserializing them in the child process. This can cause problems if the parent process contains globals that are not serializable. See the section called “@preserveGlobalState” for information on how to fix this. – source

Disabling @preserveGlobalState for True Isolation

As noted above, there is another declaration – @preserveGlobalState – that is the other half of the solution to aliasing namespaces per test suite. According to the source,

When a test is run in a separate process, PHPUnit will attempt to preserve the global state from the parent process by serializing all globals in the parent process and unserializing them in the child process. This can cause problems if the parent process contains globals that are not serializable. To fix this, you can prevent PHPUnit from preserving global state with the @preserveGlobalState annotation. – source

I actually want to disable this. That makes sense, right, because to isolate a separate PHP process I don’t want to unserialize and include the global state of any other process. This can be done with @preserveGlobalState disabled.

Where to Perform the Namespace Aliasing

You get the benefit of my trial and error here. Inside the test suite PHP file, what doesn’t work is

  1. Having any use namespace declarations of the class to be mocked
  2. Calling class_alias() outside of the test suite class

What does work is

  1. Calling class_alias() inside setUpBeforeClass()
  2. Calling class_alias() inside setUp()
However, setUpBeforeClass() will actually be called once for each test in the suite in contrast to its advertized normal behaviour of being called just once for the test suite. If you alias inside this method, be sure to add a guard to prevent the alias being called multiple times.

I prefer aliasing inside setUp(). That object method is called once per test, but it is completely isolated from the other tests and other test suites.

Illustrated Example

Putting it altogether:

As a proof of concept, I’ve added echo __METHOD__ . PHP_EOL; in two locations to echo out when the setUp() method is called and when the constructor of the mock class is called. Here are the successful results:

PHPUnit annotations for namespace aliasing
PHPUnit annotations for namespace aliasing
PHPUnit annotations for namespace aliasing results
PHPUnit results
One drawback is that Xdebug breakpoints will no longer work as each test is run in a separate process without having the CLI Xdebug flags passed in as well. Without any obvious modifications or annotations, the easiest way to debug is to remove the above annotations and run only this test suite. Once satisfied with the test suite, add them back.

Alias Namespaces Per Test

This technique can be extended, say, if I want to mock more classes in individual tests. The principle is identical, and class_alias() can also be called in the test itself without any more annotations. This is because under the hood each test is run in a separate process. You can safely alias classes on a per-test basis as well.

IDE Warning: Multiple Definitions Exist

PhpStorm is wonderful. It even detects that multiple definitions of a class exist. I’ve inadvertently created multiple definitions of a class with class_alias(). If your IDE is like PhpStorm, then you’ve seen these warnings too. You can safely ignore them, but they are easy to silence. Simply break up the string literal in the class_alias() function call:

Results

This technique of mocking hard dependencies is elegant. It is unnecessary to use libraries like Mockery with exotic overloading, ReflectionClass or the in-built PHPUnit mocking methods to achieve sophisticated class mocking. As long as the hard dependencies are autoloaded and not declared in the test suite, this simple but effective mocking works wonders.

Notes:

  1. I’m using PHPUnit 6.3 at this time with it’s modern namespacing