Skip to main content

Registering your PHPUnit test as an event subscriber for testing events

Published on

I love leveraging events in my application architecture. Drupal uses the EventDispatcher component from Symfony, which implements the Mediator and Observer design patterns. This allows for business logic to be extensible without making systems entirely coupled. The originating system dispatches an event and allows any other system to react to that event or modify data associated with that event. The originating system can then perform other interactions after its observers have processed the event.

Most of the time, we write tests for our subscribers to an event, as we subscribe to existing events in our custom modules. But, what if our custom code is introducing new events? A common pattern that I have seen is creating a test module that defines an event subscriber for the event that then pushes some data into the application state, which the test then reads. I believe there is a better way to reduce the amount of boilerplate code!

Did you know you can register your test class as an event subscriber and handle subscribing to dispatched events?

Note! This will not work with Unit tests, as there isn't a bootstrapped Drupal kernel, which requires a database connection. This is intended for Kernel tests, which have a minimally bootstrapped Drupal kernel.

The base test class for Kernel tests, \Drupal\KernelTests\KernelTestBase, implements \Drupal\Core\DependencyInjection\ServiceProviderInterface. Drupal's dependency injection implementation uses this to allow classes to state that they register services to the service container. The ::bootKernel method adds the class as a service provider 

    // Add this test class as a service provider.
    $GLOBALS['conf']['container_service_providers']['test'] = $this;

Drupal reads from the container_service_providers array and verifies they implement ServiceProviderInterface and call their register method. The following is an example of the register implementation in a test class. We create a new service definition and tag it as an event subscriber, and then set the service as our current class.

  // Register our class with the service container as an event subscriber.
  public function register(ContainerBuilder $container): void {
    parent::register($container);
    $container
      ->register('testing.config_save_subscriber', self::class)
      ->addTag('event_subscriber');
    $container->set('testing.config_save_subscriber', $this);
  }

The test must also implement \Symfony\Component\EventDispatcher\EventSubscriberInterface and its required method of getSubscribedEvents. In this following snippet, we'll declare our subscribed events and create the method to be called when the event is dispatched that collects dispatched events.

  // Track caught events in a property for testing.
  private array $caughtEvents = [];

  // Set our `catchEvents` for the subscribing method.
  public static function getSubscribedEvents(): array {
    return [ConfigEvents::SAVE => 'catchEvents'];
  }

  // Push events into our property for testing.
  public function catchEvents(ConfigCrudEvent $event): void {
    $this->caughtEvents[] = $event;
  }

Our catchEvents method is the subscribing method and pushes each event into a property that we can test in a test method, as shown below:

  // Test saving config fires the event and that we caught it.
  public function testEventsCaught(): void {
    self::assertCount(0, $this->caughtEvents);
    $this->config('system.theme')->set('default', 'test_theme')->save();
    self::assertCount(1, $this->caughtEvents);
  }

I hope this was a neat treat and added something to your tool belt when testing your Drupal code!

A complete code example is below. This listens to events dispatched whenever a configuration object is saved.

<?php declare(strict_types=1);

namespace Drupal\Tests\mymodule\Kernel;

use Drupal\Core\Config\{ConfigCrudEvent, ConfigEvents};
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class EventSubscriberTest extends KernelTestBase implements EventSubscriberInterface {

  protected static $modules = ['system'];

  // Track caught events in a property for testing.
  private array $caughtEvents = [];

  // Set our `catchEvents` for the subscribing method.
  public static function getSubscribedEvents(): array {
    return [ConfigEvents::SAVE => 'catchEvents'];
  }

  // Push events into our property for testing.
  public function catchEvents(ConfigCrudEvent $event): void {
    $this->caughtEvents[] = $event;
  }

  // Register our class with the service container as an event subscriber.
  public function register(ContainerBuilder $container): void {
    parent::register($container);
    $container
      ->register('testing.config_save_subscriber', self::class)
      ->addTag('event_subscriber');
    $container->set('testing.config_save_subscriber', $this);
  }

  // Test saving config fires the event and that we caught it.
  public function testEventsCaught(): void {
    self::assertCount(0, $this->caughtEvents);
    $this->config('system.theme')->set('default', 'test_theme')->save();
    self::assertCount(1, $this->caughtEvents);
  }

}

 

I'm available for one-on-one consulting calls – click here to book a meeting with me 🗓️