Improve your PHP code testability

Fabio Hiroki
4 min readSep 20, 2021

--

A picture of a person programming

Introduction

Welcome, hope you enjoy!

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.

Challenge accepted

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?

What is reflection?

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 of our project

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;
}

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.

We did it!

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.

--

--

Responses (5)