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 - «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 - 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();
I don’t know much, but I think it loads file via stream and removes the
T_FINAL
token.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