Anonymous classes in PHP 7 are fantastic for tests

Warning: This post is over a year old. The information may be out of date.

I recently found myself writing some integration tests (or whatever you’d like to call them, people will argue about terminology for hours) for an app. Anonymous classes made testing the real implementation of my interface really easy, here’s how.

I had an event dispatcher interface in my domain:

src/Domain/Event/EventDispatcher.php

<?php

namespace App\Domain\Event;

interface EventDispatcher
{

    public function dispatch(Event $event);

    public function addListener(string $eventName, Listener $listener);
}

I had an abstract class for an Event that contained some helper methods (though it could just be an interface, but that’s not relevant to this) and a Listener interface that had a couple of methods (for note later on in this post):

src/Domain/Event/Listener.php

<?php

namespace App\Domain\Event;

interface Listener
{
    /**
     * Called when this listener should handle an event that's dispatched
     *
     * @param Event $event
     *
     * @return void
     */
    public function handle(Event $event);
}

To use this in my application, I had to have an implementation of this, for my event dispatcher I was using League\Event.

src/Infrastructure/Event/LeagueEventDispatcher.php

<?php

namespace App\Infrastructure\Event;

use App\Domain\Event\EventDispatcher;
use App\Domain\Event\Event;
use App\Domain\Event\Listener;
use League\Event\Emitter;

class LeagueEventDispatcher implements EventDispatcher
{

    /**
     * @var Emitter
     */
    private $emitter;

    public function __construct(Emitter $emitter)
    {
        $this->emitter = $emitter;
    }

    public function dispatch(Event $event)
    {
        $this->emitter->emit($event);
    }

    public function addListener(string $eventName, Listener $listener)
    {
        $this->emitter->addListener($eventName, $listener);
    }
}

Now, in the places I was typehinting my EventDispatcher - I could mock the dependencies, as you would.

But I needed to write an integration test to make sure that my LeagueEventDispatcher worked - it’s only a simple implementation, but I wanted to make sure I’d actually put all the parts together. Mocking the Emitter isn’t an option, since I’m making assumptions about how it works (plus don’t mock what you don’t own)

To write an integration tests for this, I’d need to have an event to dispatch, as well as a listener that listened for that event.

To accomplish this, I could use a real listener and event (that is, things that are in classes that might only be used in tests, but that exist as autoloadable classes) - but I decided on another route, I could just use some simple anonymous classes to perform my tests:

tests/Integration/Infrastructure/Event/LeagueEventDispatcherTest.php

<?php

namespace Tests\Integration\Infrastructure\Event;

use App\Infrastructure\Event\LeagueEventDispatcher;
use App\Domain\Event\Event;
use App\Domain\Event\Listener;
use League\Event\Emitter;
use PHPUnit\Framework\TestCase;

class LeagueEventDispatcherTest extends TestCase
{

    /**
     * @test
     */
    public function it_emits_events()
    {
        // arrange
        $listener = new class implements Listener
        {
            protected $events = [];

            public function handle(Event $event)
            {
                $this->events[] = $event;
            }

            public function getEvents(): array
            {
                return $this->events;
            }
        };
        $event = new class('test_event') extends Event {};

        $dispatcher = new LeagueEventDispatcher(new Emitter);
        $dispatcher->addListener('test_event', $listener);

        // act
        $dispatcher->dispatch($event);

        // assert
        self::assertCount(1, $listener->getEvents());
        self::assertEquals('test_event', $listener->getEvents()[0]->getName());
    }
}

Let’s break it down

Arrange

The listener

The job of this listener is to simply append the event to an array of events when it’s called. There’s a method that returns the events it’s captured, this will allow us to make an assertion in the test as to whether the event has been dispatched.

$listener = new class implements Listener
{
    protected $events = [];

    public function handle(Event $event)
    {
        $this->events[] = $event;
    }

    public function getEvents(): array
    {
        return $this->events;
    }
};

The event

The event here is really just so we can dispatch an object of the correct type. It just needs to be constructured with a name and that’s it. We make sure the event is named the same as the name the listener will be looking for when it’s added in just a moment.

$event = new class('test_event') extends Event {};

The event dispatcher

Here we construct the actual dispatcher we’ll be using for this test, it’s a LeagueEventDispatcher so it needs the Emitter to be able to do what it needs to (no mocking here!)

We also then add our listener to listen for events named test_event

$dispatcher = new LeagueEventDispatcher(new Emitter);
$dispatcher->addListener('test_event', $listener);

Act

Dispatching the event

Now we dispatch the event that we created a few lines above - if all goes well, our handler will have handled this event and we can assert that it has in the code that follows

$dispatcher->dispatch($event);

Assert

Now we just need to assert that our listener has got the event we dispatched (which if we implemented our LeagueEventDispatcher correctly, it should have! We check that we have just 1 event in $listener->getEvents() and that the event name matches the event that was dispatched.

self::assertCount(1, $listener->getEvents());
self::assertEquals('test_event', $listener->getEvents()[0]->getName());

Now I have a test that covers the implementation of the LeagueEventDispatcher - so I can rest knowing that I’ve not had to mock any 3rd party code, that the other areas of my app can use the mocked interface, and at the same time I have an implementation that does actually work.

Job done!