PHPUNIT & CODE QUALITY

How to make PHPUnit, Mocking and Final Classes coexist together

Do not sacrifice your code’s quality because of your test framework.

Jason Benedetti
Byborg Engineering
Published in
3 min readApr 30, 2021

--

Introduction

I know the title sounds great, doesn’t it?!

Our choice to respect the best practices, SOLID principles, and the Composition Over Inheritance approach as much as possible should never be affected by any issues with our test framework(s). As a developer, I should never remove a “final” statement just because “I don’t know how I will test it if I keep it”.

How to achieve this craziness?

Let’s imagine we have a very simple application that uses PHP ≥ 7.1, Composer, and PHPUnit.

In this imaginary situation, we have a class User with ID and Currency as its mandatory parameters. We decided to use a VO (Value Object) to represent and verify the currency. We also decided to declare it “final”, because we don’t want to allow the rest of our application to extend it. It will look something like this:

User.php

class User
{
private int $id;

private Currency $currency;

public function __construct(int $id, Currency $currency)
{
$this->id = $id;
$this->currency = $currency;
}

public function getId(): int
{
return $this->id;
}

public function getCurrency(): Currency
{
return $this->currency;
}
}

Currency.php

final class Currency
{
private const VALID_VALUES = ['eur', 'dol'];

private string $value;

public function __construct(string $value)
{
if (!in_array($value, self::VALID_VALUES, true)) {
throw new InvalidArgumentException('...');
}

$this->value = $value;
}

public function getValue(): string
{
return $this->value;
}
}

Now, we want to add a (very simple) unit test for the User class:

UserTest.php

class UserTest extends PHPUnit\Framework\TestCase
{
public function testCreation(): void
{
$mockedCurrency = $this->createMock(Currency::class);

$sut = new User(123, $mockedCurrency);

self::assertEquals(123, $sut->getId());
self::assertSame($mockedCurrency, $sut->getCurrency());
}
}

Problem! After running our tests, we see we have an error. PHPUnit throws us a “ClassIsFinalException” because, indeed, it’s impossible to mock a final class since the mocking process creates an inheritance.

We don’t want to use a real Currency object because a unit test should not directly depend on an object, as it isn’t a System Under Test (SUT). Moreover, if the Currency::construct() method needs another parameter or will stop supporting a specific value in the future, we really don’t want to rewrite all the tests where we introduced a real instantiation instead of using a mock one.

One library to mock them all

Because PHPUnit is such a great tool, it allows us to write custom extensions. We will use these and the amazing dg/bypass-finals library to allows us to mock final classes with just a few lines of code.

First, let’s get the library with Composer:

composer require --dev dg/bypass-finals

Now, we can create a PHPUnit Hook and add it to our phpunit.xml file:

ByPassFinalHook.php

<?php

class ByPassFinalHook implements PHPUnit\Runner\BeforeTestHook
{
public function executeBeforeTest(string $test): void
{
// mutate final classes into non final on-the-fly
DG\BypassFinals::enable();
}
}

phpunit.xml

<phpunit bootstrap="vendor/autoload.php">
<extensions>
<extension class="ByPassFinalHook" />
</extensions>
</phpunit>

And that’s it! We can now run again our tests again and… it works!

Disadvantages

This hook approach will slow down your tests. So, if you have a lot of tests, you can choose to avoid using a PHPUnit Hook, and instead call the BypassFinals::enable() method in the specific tests where you need it, such as the in the setUp() method of the test class for example.

Enjoy!

--

--