How to Mock Final Classes in PHPUnit

Do you prefer composition over inheritance? Yes, that's great. Why aren't your classes final then? Oh, you have tests and you mock your classes. But why is that a problem?

Since I started using final first I got rid of many problems. Most programmers I meet already know about the benefits of not having 6 classes extended in a row and that final remove this issue.

But many of those programmers are skilled and they write tests.

How Would You Mock this Class?

...so it returns 20 on getNumber() instead:

<?php

final class FinalClass
{
    public function getNumber(): int
    {
        return 10;
    }
}

We have few options out in the wild:

or...

Extract an Interface

 <?php

-final class FinalClass
+final class FinalClass implements FinalClassInterface
 {
     public function getNumber(): int
     {
         return 10;
     }
 }
+
+interface FinalClassInterface
+{
+    public function getNumber(): int;
+}

Then use the interface instead of the class in your test:

 <?php

 use PHPUnit\Framework\TestCase;

 final class FinalClassTest extends TestCase
 {
     public function testSuccess(): void
     {
-        $finalClassMock = $this->createMock(FinalClass::class);
+        $finalClassMock = $this->createMock(FinalClassInterface::class);
         // ... it works! but at what cost...
     }
 }

This will work, but creates huge debt you'll have to pay later (usually at a time you would rather skip):

This is obviously annoying maintenance and it will lead you to one of 2 bad paths:

By Pass Finals!

Nette packages also missed final in the code, so people could mock it. Until David came with Bypass Finals package. Some people think it's only for Nette\Tester, but I happily use it in PHPUnit universe as well.

We just install it:

composer require dg/bypass-finals --dev

And enable:

DG\BypassFinals::enable();

Hm, where should be put it?

1. bootstrap.php File?

require_once __DIR__ . '/../vendor/autoload.php';

DG\BypassFinals::enable();

Update path in phpunit.xml:

 <phpunit
-    bootstrap="vendor/autoload.php"
+    bootstrap="tests/bootstrap.php"
 >

Let's run the tests:

vendor/bin/phpunit

...

OK (3 tests, 3 assertions)

Hm, mocks are worked, and let's try another approach.

2. setUp() Method?

Let's put it into setUp() method. It seems like a good idea for these operations:

 <?php

+use DG\BypassFinals;
 use PHPUnit\Framework\TestCase;

 final class FinalClassTest extends TestCase
 {
+    protected function setUp(): void
+    {
+        BypassFinals::enable();
+    }

     public function testFailInside(): void
     {
         $this->createMock(FinalClass::class);
     }
 }

And run tests again:

vendor/bin/phpunit

...

OK (3 tests, 3 assertions)

We're getting there, but there are still mocks in the setUp() method, and we've also added work to our future self - for every new test case, we have to remember to add BypassFinals::enable(); manually.



Why it doesn't work. I was angry and frustrated. Honestly, I wanted to give up now and just pick "interface everything" or "final nothing" quick solution. I think that resolutions in emotions are not a good idea... so I take a deep breath, pause and go to a toilet to get some fresh air.


Suddenly... I remember that... PHPUnit has some Listeners, right? What if we could use that?

3. Own TestListener?

Let's try all the methods of TestListener, enable bypass in each of them by trial-error and see what happens:

<?php declare(strict_types=1);

use DG\BypassFinals;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestListener;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;

final class BypassFinalListener implements TestListener
{
    public function addError(Test $test, \Throwable $t, float $time): void
    {
    }

    public function addWarning(Test $test, Warning $e, float $time): void
    {
    }

    public function addFailure(Test $test, AssertionFailedError $e, float $time): void
    {
    }

    public function addIncompleteTest(Test $test, \Throwable $t, float $time): void
    {
    }

    public function addRiskyTest(Test $test, \Throwable $t, float $time): void
    {
    }

    public function addSkippedTest(Test $test, \Throwable $t, float $time): void
    {
    }

    public function startTestSuite(TestSuite $suite): void
    {
    }

    public function endTestSuite(TestSuite $suite): void
    {
    }

    public function startTest(Test $test): void
    {
        BypassFinals::enable();
    }

    public function endTest(Test $test, float $time): void
    {
    }
}

In the end, it was just one method.

Then register listener it in phpunit.xml:

<phpunit bootstrap="vendor/autoload.php">
    <listeners>
        <listener class="Listener\BypassFinalListener"/>
    </listeners>
</phpunit>

And run tests again:

vendor/bin/phpunit

...

Success!

Great! All our objects can be final and tests can mock them.

Is it a good enough solution? Yes, it works and it's a single place of origin - use it, close this post and your code will thank you in 2 years later.


Are you a curious hacker that is never satisfied with his or her solution? Let's take it one step further.

What do you think about the Listener class? There is 10+ methods and only one is used. It's very hard to read. To add more fire to the fuel, TestListener class is deprecated since PHPUnit 8 and will be removed in PHPUnit 9. Don't worry, Rector already covers the migration path.

After bit of Googling on PHPUnit Github and documentation I found something called hooks!

4. Single Hook

You can read about them in the PHPUnit documentation, but in short: they're the same as the listener, just with 1 event.

<?php declare(strict_types=1);

use DG\BypassFinals;
use PHPUnit\Runner\BeforeTestHook;

final class BypassFinalHook implements BeforeTestHook
{
    public function executeBeforeTest(string $test): void
    {
        BypassFinals::enable();
    }
}

And again, register it in phpunit.xml:

<phpunit bootstrap="vendor/autoload.php">
    <extensions>
        <extension class="Hook\BypassFinalHook"/>
    </extensions>
</phpunit>

The final test, run all tests:

vendor/bin/phpunit

...

Success!

✅ ✅ ✅

Before

After


Finally :)


Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!