Improve your PHP code testability

Fabio Hiroki - Sep 19 '21 - - Dev Community

Introduction

gif of a man welcoming you
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.

GitHub logo fabiothiroki / php-reflection-test

A simple example of unit tests using reflection

Challenge accepted

gif written 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?

gif written 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?

Class diagram

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 Implementationis 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;
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Set the unacessibleDependency of the previous instance accessible by reflection:

$reflectionClass = new ReflectionClass($this->implementation);
$property = $reflectionClass->getProperty('unacessibleDependency');
$property->setAccessible(true);
Enter fullscreen mode Exit fullscreen mode

Create a mock instance of UnacessibleDependency so we can verify if it's being called:

$this->mockUnacessible = $this->createMock(UnacessibleDependency::class);
Enter fullscreen mode Exit fullscreen mode

Then substitute the original depedency by the mock one:

$property->setValue($this->implementation, $this->mockUnacessible);
Enter fullscreen mode Exit fullscreen mode

Finally, the assertion

$this->mockUnacessible->expects(self::once())
            ->method('doSomethingElse')
            ->with(self::equalTo('argument'));

$this->implementation->doSomething('argument');
Enter fullscreen mode Exit fullscreen mode

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.

celebration gif

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . .