Converting a PHPUnit TestListener to an Event Subscriber
One of the bigger changes in PHPUnit 10 was the introduction of the new extension system which replaced listeners and hooks.
The old way
On one of my projects we have a TestListener that sets up the database before we run some functional tests against it.
It looks like this:
<?php declare(strict_types=1); namespace App\Test\Listeners; use App\Test\Functional\Helpers\DbHelper; use PHPUnit\Framework\TestListenerDefaultImplementation; use PHPUnit\Framework\TestSuite; class TestListener implements \PHPUnit\Framework\TestListener { use TestListenerDefaultImplementation; public function startTestSuite(TestSuite $suite): void { if (str_contains($suite->getName(), 'functional')) { DbHelper::setup(); } } }
As the PHPUnit TestListener interface defines a lot of methods, you can use the TestListenerDefaultImplementation trait to stub them all out and only write the ones you care about. In our case, we care about startTestSuite() as that’s run when a test suite is started (the clue is in the name!). Our implementation is trivial: if the name of the suite is functional, then call DbHelper::setup().
It’s registered in phpunit.xml like this:
<listeners> <listener class="App\Test\Listeners\TestListener"></listener> </listeners>
The new way
TestListener was removed in PHPUnit 10 and so we had to replace this with a new extension. You need an Extension class to that is registered with PHPUnit and in turn registers as many Subscriber classes you need as a subscriber can only lister to one event.
The Extension
I called our extension SetupDatabaseBeforeFunctionalTestsExtension. It looks like this:
<?php declare(strict_types=1); namespace App\Test\Extension; use App\Test\Helper\DbHelper; use PHPUnit\Runner\Extension\Extension; use PHPUnit\Runner\Extension\Facade; use PHPUnit\Runner\Extension\ParameterCollection; use PHPUnit\TextUI\Configuration\Configuration; final class SetupDatabaseBeforeFunctionalTestsExtension implements Extension { public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void { $facade->registerSubscribers( new SetupDatabaseBeforeFunctionalTests() ); } }
This class implements the Extension interface which means that we need to implement the bootstrap() method. You can set up things in here and then register your subscribers. In our case, we only need to register the SetupDatabaseBeforeFunctionalTests subscriber.
To register an extension with PHPUnit, you add it to phpunit.xml, like this:
<extensions> <bootstrap class="App\Test\Extension\SetupDatabaseBeforeFunctionalTestsExtension"/> </extensions>
The implementation of SetupDatabaseBeforeFunctionalTests is relatively simple:
<?php declare(strict_types=1); namespace App\Test\Extension; use App\Test\Helper\DbHelper; use PHPUnit\Event; use PHPUnit\Event\TestSuite\Started; final class SetupDatabaseBeforeFunctionalTests implements Event\TestSuite\StartedSubscriber { public function notify(Started $event): void { if (str_contains($event->testSuite()->name(), 'functional')) { DbHelper::setup(); } } }
A subscriber implements the relevant interface for the event that you want to subscribe to. There are many events, which are listed in the docs and you can infer the interface name by appending the word “Subscriber” to the event name.
In our case, we want to subscribe to the PHPUnit\Event\TestSuite\Started event, so we implement the PhpUnit\Event\TestSuite\StartedSubscriber interface. Our subscriber then implements the notify() method where the $event is passed in. Unsurprisingly, the code for our notify() is essentially the same as we used in our TestListener.
That’s it. We’re done.
A simplification to one class
As is clear by the name of my extension, this extension will only ever do one thing; set up the database before running the functional test suite. For a case like this, we can consolidate to a single class by creating an anonymous subscriber class and put it directly into the extension class:
<?php declare(strict_types=1); namespace App\Test\Extension; use App\Test\Helper\DbHelper; use PHPUnit\Event\TestSuite\Started; use PHPUnit\Event\TestSuite\StartedSubscriber; use PHPUnit\Runner\Extension\Extension; use PHPUnit\Runner\Extension\Facade; use PHPUnit\Runner\Extension\ParameterCollection; use PHPUnit\TextUI\Configuration\Configuration; final class SetupDatabaseBeforeFunctionalTestsExtension implements Extension { public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void { $facade->registerSubscribers( new class implements StartedSubscriber { public function notify(Started $event): void { if (str_contains($event->testSuite()->name(), 'functional')) { DbHelper::setup(); } } } ); } }
Nothing else changes and my database is set up before we run the test suite and I can now upgrade the rest of my tests to support PHPUnit 10.