# Stop mocking about: Event Dispatcher
*It's time to replace your mocked event dispatchers with a real one*

August 15, 2022 — by Doeke Norg

---

I don't like mocks in unit testing (I use them, but don't like them). It often results in tests that only assure some 
specific method was called with certain parameters. It is this approach that may lead some to think that 
writing tests is writing the code twice, and therefore a waste of time. It also leaves a lot of room for errors, 
missing tests, or broken tests when the implementation is changed.

That's why I wanted to make a miniseries on replacing mocks with real implementations by providing some semi-real-world
examples.

In this post we'll be looking into testing Event Dispatchers & Event Listeners. First we'll use mocks and check out
some of their shortcomings. Then we'll rewrite to real implementations and see what makes them better.

## Replacing a mocked event dispatcher

Let's imagine we have an `Importer` service that has a `getPostTitle(array $post): string` method. This method returns
the title of the provided `$post` array. But before doing so; it dispatches a `GetTitleEvent` to be able to overwrite
this title before it is returned.

*Yes, this is a contrived example, but it's simple; and designed to get the point across.*

We'll be using the [`league/event`](https://event.thephpleague.com) package for the event dispatcher,
and `phpunit/phpunit` for the unit tests.

> **Note:** The full code of this example 
> [can be found on GitHub](https://github.com/doekenorg/stop-mocking-about-event-dispatcher).

Let's set up the `GetTitleEvent` and the `Importer` first.

```php
# src/Event/GetTitleEvent.php

namespace App\Event;

class GetTitleEvent
{
    public function __construct(private string $title)
    {
    }

    public function setTitle(string $title): self
    {
        $this->title = $title;

        return $this;
    }

    public function getTitle(): string
    {
        return $this->title;
    }
}
```

```php
# src/Service/Importer.php

namespace App\Service;

use App\Event\GetTitleEvent;
use Psr\EventDispatcher\EventDispatcherInterface;

class Importer
{
    public function __construct(private EventDispatcherInterface $dispatcher) {}

    public function getPostTitle(array $post): string
    {
        $title = $post['title'] ?? '';

        $event = $this->dispatcher->dispatch(new GetTitleEvent($title));

        return $event->getTitle();
    }
}

```

These classes are pretty straight-forward. The `GetTitleEvent` has a `setTitle` method to change the title; and
a `getTitle` method to return the title. The `Importer` dispatches this event, and uses the `getTitle` method and
returns the value from the event.

### Testing the importer with a mocked event dispatcher

Now let's look at an example of how the `Importer` could be tested using a mocked `EventDispatcherInterface`.

```php
# tests/Service/ImporterTest.php

use App\Event\GetTitleEvent;
use App\Service\Importer;
use PHPUnit\Framework\TestCase;
use Psr\EventDispatcher\EventDispatcherInterface;

class ImporterTest extends TestCase
{
    public function testGetPostTitle(): void
    {
        $dispatcher = $this->createMock(EventDispatcherInterface::class);
        $importer = new Importer($dispatcher);

        $dispatcher
            ->expects(self::once())
            ->method('dispatch')
            ->with($event = new GetTitleEvent('The title'))
            ->willReturn($event);

        self::assertSame('The title', $importer->getPostTitle(['title' => 'The title']));
    }
}
```

This test works, and it has full test coverage. But what are we actually testing here? Because we've mocked
the `EventDispatcherInterface`, we need to tell the mock what to expect and return:

It will receive exactly one call to `dispatch` with a `GetTitleEvent` that has `The title` as a value, and it needs 
to return that event; because we will call `getTitle()` on it. 

So we are mostly building a fake event dispatcher, and testing if it gets the correct parameters. Only then can we 
test the actual function of this test `Importer::getPostTitle()` and assert it returns the proper 
value.

So more than half of this test is making sure the `getPostTitle` method calls some exact code; and sets up the world so
that the actual test can be performed. And it doesn't even listen to the event, or change the title.

### Testing the importer using a real event dispatcher

Now let's look at what this test would look like, if we substituted the mock with a real event dispatcher.

```php
# tests/Service/ImporterTest.php

use App\Service\Importer;
use League\Event\EventDispatcher;
use PHPUnit\Framework\TestCase;

class ImporterTest extends TestCase
{
    public function testGetTitle(): void
    {
        $importer = new Importer(new EventDispatcher());

        self::assertSame('The title', $importer->getPostTitle(['title' => 'The title']));
    }
}
```

Here we create the `Importer` with a new instance of the `league/event` event dispatcher. This dispatcher already works
according to the interface, so we don't have to tell it what to expect or what to return. We can focus on the 
function at hand, and assert that the title is correct. We can even update the `Importer` to call other methods on 
the event dispatcher, and this test would still be working and be valid. Even the coverage is still 100%. So less 
code, and more succinct testing.

But now we are no longer testing whether the code is actually using the event dispatcher. We could remove the dispatcher
call, and this test would still be valid. So we need to dive in a bit deeper. For this we'll start looking in to a
listener.

## Replacing a mocked event

Since we are dispatching an event to change the title; let's create a `OverwriteTitleListener` that, well, overwrites
the title on a `GetTitleEvent`.

```php
# src/EventListener/OverwriteTitleListener.php

namespace App\EventListener;

use League\Event\Listener;

class OverwriteTitleListener implements Listener
{
    public function __invoke(object $event): void
    {
        $event->setTitle('Overwritten');
    }
}
```

Here we have a listener that will overwrite the title to be `Overwritten`. Now let's test this class using a mocked 
event object.

### Testing the listener with a mocked event object

```php
# src/tests/EventListener/OverWriteTitleListenerTest.php

use App\Event\GetTitleEvent;
use App\EventListener\OverwriteTitleListener;
use PHPUnit\Framework\TestCase;

class OverWriteTitleListenerTest extends TestCase
{
    public function testListener(): void
    {
        $listener = new OverwriteTitleListener();
        $event = $this->createMock(GetTitleEvent::class);
        $event
            ->expects(self::once())
            ->method('setTitle')
            ->with('Overwritten');

        $listener($event);
    }
}
```

Our test case creates a mocked `GetTitleEvent` and tells it, it should expect a call to `setTitle` with the
value `Overwritten`. While this test is valid, and again will have a coverage of 100%, it is only asserting that 
some exact code was executed. It does **not** test whether the listener actually changed the title.

### Testing the listener with a real event object

To make sure our listener changes the title on the event, we need to use a real event object. So let's rewrite this 
test.

```php
# src/tests/EventListener/OverWriteTitleListenerTest.php

use App\Event\GetTitleEvent;
use App\EventListener\OverwriteTitleListener;
use PHPUnit\Framework\TestCase;

class OverwriteTitleListenerMockTest extends TestCase
{
    public function testListener(): void
    {
        $listener = new OverwriteTitleListener();
        $event = new GetTitleEvent('Some title');

        $listener($event);

        self::assertSame('Overwritten', $event->getTitle());
    }
}
```

By using a real `GetTitleEvent` we no longer need to tell the object what to expect or how to function. And why should
we? It already knows what to do, because we built it that way.

> **Note:** If you want to be more certain the code actually works, you could add another assertion before the 
> `$listener($event)` call, that asserts `getTitle` is still `Some title`. But these tests should probably live in a
> dedicated test class for `GetTitleEvent`.

## Testing the actual implementation

So now we know our listener works when it receives the proper event; and we know our importer returns the proper value
when it is invoked. But we still can't be sure the listener is triggered when the title is retrieved. So let's write a
test that confirms the implementation fully works.

Because we already have a test that confirms the default behavior is working, let's add another test that confirms the
event is dispatched and being listened to.

```php
# tests/Service/ImporterTest.php

use App\Event\GetTitleEvent;
use App\EventListener\OverwriteTitleListener;
use App\Service\Importer;
use League\Event\EventDispatcher;
use PHPUnit\Framework\TestCase;

class ImporterTest extends TestCase {
    // ...
  
    public function testGetTitleWithListener(): void
    {
        $dispatcher = new EventDispatcher();
        $dispatcher->subscribeTo(GetTitleEvent::class, new OverwriteTitleListener());
        $importer = new Importer($dispatcher);

        self::assertSame('Overwritten', $importer->getPostTitle(['title' => 'Some title']));
    }
}
```

In this test we use a real event dispatcher with a real event listener. We hook the listener up to the dispatcher, and
then perform the `getPostTitle` call. Aside from the code being ridiculously short, it makes 100% sure that the event
dispatcher is called, the right event is dispatched, and the correct result is being returned. We don't have to mock any
class, or tell it to expect a certain function call; we just call the real code and assert the actual result.

> **Note:** In this example I'm reusing the `OverwriteTitleListener` only for illustrative purposes. However, for 
> unit tests, you should not rely on other parts of your code. An anonymous callback function could replace the 
> listener in this example. In its current state, you could say this was an integration test.

Now let's checkout this example using only mocks to test the full implementation.

```php
public function testGetTitleWithListener(): void
{
    $dispatcher = $this->createMock(EventDispatcherInterface::class);
    $importer = new Importer($dispatcher);

    $event = $this->createMock(GetTitleEvent::class);
    $dispatcher
        ->expects(self::once())
        ->method('dispatch')
        ->willReturn($event);

    $event
        ->expects(self::once())
        ->method('getTitle')
        ->willReturn('Overwritten');
  
    self::assertSame('Overwritten', $importer->getPostTitle(['title' => 'some title']));
}
```

That a lot of extra code, while actually testing a bit less. Because we are mocking the dispatcher and the event, we
cannot be a 100% sure the code actually works like that; we only tell the test that it does. So even if we break the
actual implementation, for example by changing the return type of `dispatch` or the result of `getTitle`, it will not
break this test. But it will break the test that uses the actual code.

## Conclusion

While mocking can be extremely easy to write a few tests; it often requires a lot of repeated expectations, setup and
returned values. I've seen mocks that were so complex (because some methods were called multiple times, with
different parameters and different results) that the actual test could not be understood or trusted.

By implementing the real classes your test code gets smaller, easier to digest, and it tests the actual implementation 
instead of a fake world in which class A lies to class B about what it does.

To force yourself to avoid mocks, try adding `final` to your classes when possible. Final classes cannot be mocked,
because the class cannot be extended. In the next post in this series, we'll look into a technique you can use to use
real `final` implementations, and add assertions to them.

*I hope you enjoyed reading this article! If so, please leave a 👍 reaction or a 💬 comment and consider subscribing to
my newsletter! You can also follow me on 🐦 [twitter](https://twitter.com/intent/follow?screen_name=doekenorg) for more
content and the occasional tip.*
