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?

<?php declare(strict_types=1);

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

...

There were 19 warnings:

1) SomeClassTest::testSomeMethod
Class "SomeClass" is declared "final" and cannot be mocked.

Hm, most mocks work, but there are still some errors.

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
 {
+    public function setUp()
+    {
+        BypassFinals::enable();
+    }

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

And run tests again:

vendor/bin/phpunit

...

There were 7 warnings:

1) AnotherClassTest::testSomeMethod
Class "AnotherClass" is declared "final" and cannot be mocked.

Damn you, black magic! 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 :)


Do you want to see solutions 2, 3 and 4 tested in real PHPUnit code? They're here on Github


Happy coding!


  Travis Knows the Code Works

The code used in this post is tested daily with Travis CI. You can see tests on Github.

Thanks to tests this post:

  • always run against the most recent dependencies
  • gets updates and stays relevant for many years even when new major version of PHP or Symfony is released

Found a typo? Fix it to join team of 64 people that improve content here

❤️️ Do you like what I write about? Or do you hate it but enjoy discussion? 😠
You can support my writing by throwing a couple bucks at my Patreon