Introduction
Welcome to my article! I hope you are like me and believe in the power of unit tests, or at least know that it's important to write them, right? In short words, I like them because it gives me more confidence that my code is working and no one else in the future will make changes there by mistake.
In this article I want to show you how to combine mocks with reflection and write a test that seemed impossible before.
Final code is on Github.
fabiothiroki / php-reflection-test
A simple example of unit tests using reflection
Challenge accepted
I've just started my new job, in this company that uses PHP as a main language. Got a new ticket and I was excited to ship my new code (with tests, of course!).
But suddenly, a wild class appears! It doesn't expose one dependency that I needed to mock. I could refactor this code to receive this dependency by its constructor but decided to not follow this path because I was new (noob) at the company, so I didn't know if this was a common pattern, or what the original developer of this class was thinking when made this design.
The other alternative that I'm going to show here later was not supposed to be a big effort. Coming from a Java background, I knew this could be done using reflection.
Reflection? What?
Before jumping into code, we need to understand the concept of reflection programming. Here goes my simple definition:
Reflection is a tool that can be used to inspect your code and bypass static typing.
For example, in this article I will use reflection to change an instance of an unacessible attribute from a class that doesn't have a setter or another way to expose this attribute.
Why would you want to do this?
In this example, the Implementation
class has a public
method that calls a method from the unacessible dependency. So the test we want to write is this: is the dependency being called with correct arguments when public
method from Implementation
is called?
Show me the code
Abstract class
<?php
abstract class AbstractClass
{
protected $unacessibleDependency;
public function __construct()
{
$this->unacessibleDependency = new UnacessibleDependency();
}
abstract function doSomething(string $argument): void;
}
As you can see, there's no common way we can access $unacessibleDependency
from outside, unless we refactor this class, which I already said we don't want to follow this path for several reasons.
Implementation class
<?php
class ImplementationClass extends AbstractClass
{
public function doSomething(string $argument): void
{
$this->unacessibleDependency->doSomethingElse($argument);
}
}
doSomething
is the method we want to test.
Test setup
Start by creating an instance of the system under test (SUT):
$this->implementation = new ImplementationClass();
Set the unacessibleDependency
of the previous instance accessible by reflection:
$reflectionClass = new ReflectionClass($this->implementation);
$property = $reflectionClass->getProperty('unacessibleDependency');
$property->setAccessible(true);
Create a mock instance of UnacessibleDependency
so we can verify if it's being called:
$this->mockUnacessible = $this->createMock(UnacessibleDependency::class);
Then substitute the original depedency by the mock one:
$property->setValue($this->implementation, $this->mockUnacessible);
Finally, the assertion
$this->mockUnacessible->expects(self::once())
->method('doSomethingElse')
->with(self::equalTo('argument'));
$this->implementation->doSomething('argument');
Translating to human language: when doSomething
is called with argument
parameter, is doSomethingElse
being called with the same parameter?
Now we have our test, and no one got hurt during this process.
Conclusion
There's a way to add a unit test even in unlikely scenarios, but this technique should be used with caution, because we may be just adding an workaround to a poorly code design. If the code is not easily testable, it's probably not following the good practices of encapsulation and abstraction.
I want to know more!
For more techniques to make your code more testable, check out the book "Working Effectively with Legacy Code" by Michael C. Feathers.