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 package for the event dispatcher, and phpunit/phpunit for the unit tests.

Note: The full code of this example can be found on GitHub.

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

# 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;
}
}
# 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.

# 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.

# 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.

# 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

# 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.

# 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.

# 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.

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 for more content and the occasional tip.