How to Mock Final Classes in PHPUnit

5 min

Thu, Mar 28, 2019

X comments

Tested

Edit Post

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):

for every new public method in the class, you have to update the interface

method in the class, you have to update the interface "interface everything" approach will shift the meaning of interface from "something to be implemented for a reason" to "anything you want to test"

do you have 100 classes? you have 200 PHP files now, you're welcome!

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

don't use final at all

at all or do not test

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();

how BypassFinals works? Read author's

I don't know much, but I think it loads file via stream and removes the T_FINAL token. Do you want to know,Read author's blog post or check this line on Github I don't know much, but I think it loads file via stream and removes thetoken.

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

we had to use interface for mocks

or we had to remove final

we had to pick between inheritance hell or poor tests

After

A single solution, in single class

we use PHPUnit feature directly, no weird bending code

we can mock anything

we can final anything





Finally :)





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





Happy coding!