I’m working on an application that’s been around for a while. It uses a lot of static methods, and often, it instantiates objects from inside methods. This makes it a pain to test, and introducing tests is part of my job. Sometimes I’m able to refactor the code so that dependencies may be injected, but many times I can’t confidently refactor the code without breaking something somewhere else—because I don’t have enough tests. It’s a chicken-and-egg problem.
Let’s say we have a model class representing a User that looks something like this:
Testing most of this is a piece of cake. Here’s how our first stab at test cases might look:
Now, we just need to test the
getAddress() method to ensure it returns an Address object, but how are we going to do that? The
$address property is protected, so we can’t easily set it. An easy change is to set it public. Then, after instantiating a User, we can set an Address to it and assert that
getAddress() returns it.
But what if we can’t do that? What if our interface needs to stay the same?
Well, we can use reflection to change the accessibility of protected properties, so let’s try that.
Great! Now, our coverage is better, and we’ve asserted the
getAddress() method returns an Address object. The problem is we didn’t test whether the
getAddress() method sets the
$address property, and we have an ugly, untested line in our code-coverage report that’s keeping us from hitting that glorious 100% coverage mark.
Address is a hard dependency. We can’t change it. Since the object is instantiated within the method, there’s no way for us to inject a test double, so we can’t test it without fear of causing side effects. In this simple example, the side effects may be harmless—maybe it simply makes a
SELECT query against a database to retrieve data—but this turns into an integration test, and we don’t want to test our integration with the database. We only want to test our code.
One way to handle this is to admit defeat and quietly sweep away the untested lines under the rug by wrapping them with
@codeCoverageIgnoreEnd annotations. This is what I used to do when encountering code that appeared untestable.
In researching approaches to testing code, I stumbled accross the “Mocking Hard Dependencies” section of the Mockery documentation. Mockery provides the ability to inject a test double class into the scope of the running test, and when your code under test invokes that class—with the
new keyword or a static method—it uses your mock.
This works best when using an autoloader in your tests. If any code is encountered that uses a class that hasn’t yet been loaded, it invokes the autoloader to load the class. Mockery allows you to overload this behavior by injecting a class with the same name, so that when the class is encountered in your code, it is already loaded, and the autoloader doesn’t try to load your real class. If you’re using Composer’s autoloader with your tests, you’re all set.
This is not without some problems, though. If previous tests have already autoloaded the class, then Mockery won’t be able to inject a class with the same name. This will result in an error:
Mockery\Exception\RuntimeException: Could not load mock Address, class already exists
Thankfully, PHPUnit has functionality that allows us to isolate tests, so we can get around this problem. Just add the annotations
@preserveGlobalState disabled to the test (more information).
Our test using Mockery to mock the hard dependency looks like this:
Using the “overload:” prefix, Mockery creates a new class named Address and injects it into the scope of the test. Then, when the test calls
$user->getAddress() and invokes
new Address(), the Address object created uses Mockery’s mocked Address class and not our real Address class. What’s best? None of the lines in our coverage report are skipped, and every line is green!
While I’ve shown an effective way to mock hard dependencies, this can lead to brittle tests. The best approach is to use dependency injection. For legacy code, this often involves a lot of refactoring. If you’re unable to invest the time or it’s not worth the cost to refactor, then this approach to mock hard dependencies might come in handy.