409 words, 3 min read

Since PHPUnit 10, the framework introduced a powerful event system with more than 60 built-in events. These events allow us to hook into different stages of the test lifecycle, making it possible to execute custom logic before or after the tests run. While this example uses Laravel, the same approach works in any framework because we are working directly with PHPUnit’s event system.

For demonstration, we will use two events:

  • PHPUnit\Event\Application\Started — triggered when the PHPUnit CLI application starts.
  • PHPUnit\Event\Application\Finished — triggered when the PHPUnit CLI application finishes.

By subscribing to these events, we can run custom code before and after the test suite.

Inside your tests/Extension directory, create two listener classes.

tests/Extension/TestsStartedSubscriber.php

namespace Tests\Extension;
use PHPUnit\Event\Application\Started;
use PHPUnit\Event\Application\StartedSubscriber;
class TestsStartedSubscriber implements StartedSubscriber
{
public function notify(Started $event): void
{
print "PHPUnit event before tests" . PHP_EOL;
}
}

This listener will execute before any tests run.

tests/Extension/TestsFinishedSubscriber.php

namespace Tests\Extension;
use PHPUnit\Event\Application\Finished;
use PHPUnit\Event\Application\FinishedSubscriber;
class TestsFinishedSubscriber implements FinishedSubscriber
{
public function notify(Finished $event): void
{
print "PHPUnit event after tests" . PHP_EOL;
}
}

This listener will execute after all tests have finished.

Each subscriber only needs a notify method, type-hinting the corresponding event.

To let PHPUnit know about our subscribers, we need to register them via an extension class. Create an ExampleExtension class in the same directory:

tests/Extension/ExampleExtension.php

namespace Tests\Extension;
use PHPUnit\Runner\Extension\Extension;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;
class ExampleExtension implements Extension
{
public function bootstrap(
Configuration $configuration,
Facade $facade,
ParameterCollection $parameters,
): void {
$facade->registerSubscribers(
new TestsStartedSubscriber(),
new TestsFinishedSubscriber(),
);
}
}

Here, we use PHPUnit’s Facade to register our custom subscribers.

Finally, register the extension in your phpunit.xml (or phpunit.xml.dist):

<phpunit>
<extensions>
<bootstrap class="Tests\Extension\ExampleExtension"/>
</extensions>
</phpunit>

Running the tests

Now, when you run your tests:

php artisan test

You should see the custom messages printed before and after the test run.

You can use this pattern for example to verify certain things before allowing the tests to run. For example, you can use it to check the existence of a .env.testing file:

namespace Tests;
use PHPUnit\Event\Application\Started;
use PHPUnit\Event\Application\StartedSubscriber;
class TestsStartedSubscriber implements StartedSubscriber
{
public function notify(Started $event): void
{
if (!file_exists('.env.testing')) {
echo('Copy the .env.testing.example file to .env.testing before running the tests.' . PHP_EOL);
exit(1);
}
}
}

With this in place, whenever you try to run the whole test suite, a subset of it or even a single test using e.g. PhpStorm, the check will happen and abort the testing if the file isn't present.