Mocking Application Services when Integration Testing

A small point in Growing Object Oriented Software, Guided by Tests is that integration tests should use mocks for application services when possible. Let's take a look at what that means.

The code here is PHP, but the concepts should be applicable to any language.

Example Application

We're going to work with a very small part of what may be a blog application. To be specific we're concerned with posts and a post respository. Our imaginary blog application is written with Symfony.

The problem is that we find ourselves doing this a lot in controllers:

<?php

$post = $postRepository->get($postId);
if (!$post) {
    throw new NotFoundHttpException("Post {$postId} Not Found");
}

We've decided that throwing a NotFoundHttpException is a bad idea in the PostRepository for our application. Instead we're going to push the above logic into an event subscriber that inspects the incoming request for post_id attribute from routing. If found, the listener will use the post ID to fetch the post via the PostRepository. Essentially we're going to recreate the ParamConverter from SensioFrameworkExtraBundle.

Posts and the Post Repository

Here's our fake post object. It's immutable to keep our example system simple.

<?php
final class Post
{
    private $id;
    private $title;
    private $content;

    public function __construct($id, $title, $content)
    {
        $this->id = $id;
        $this->title = $title;
        $this->content = $content;
    }

    public function getId()
    {
        return $this->slug;
    }

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

    public function getContent()
    {
        return $this->content;
    }
}

And here's what we care about from the post repository.

<?php
interface PostRepository
{
    /**
     * Get a single post by its ID
     *
     * @param   int $id
     * @return  Post|null The post, if found, null otherwise.
     */
    public function get($id);

    // Probably some other methods here, like `all`, etc.
    // we don't need to worry about them for this tutorial
}

The Current Post Listener

This is about what you'd expect: takes a PostRepository in the constructor and inspects the incoming request for the post_id attribute. If it's there and the post has not already been fetched, get the post. Otherwise do nothing.

Here's the skeleton:

<?php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

final class CurrentPostListener implements EventSubscriberInterface
{
    const ID_KEY        = 'post_id';
    const OBJECT_KEY    = 'post';

    private $posts;

    public function __construct(PostRepository $posts)
    {
        $this->posts = $posts;
    }

    public static function getSubscribedEvents()
    {
        // we want to fire this AFTER routig in Symfony, which is priority 32
        return [
            KernelEvents::REQUEST => ['onRequest', 10],
        ];
    }

    public function onRequest(GetResponseEvent $event)
    {
        // ...
    }
}

Our First Tests & Testing Goals

The above is enough to write some tests. We know the behavior which is more than enough to write some tests. But what's the goal?

We could call our listener's onRequest method directly, mocking everything (the event, request, and post repository) along the way. That's a very synthentic test that missed the point. We care that our listener is interacting with the event dispatcher component properly as well as doing its job.

Rather rather test the listener directly, we should add the listener to an event dispatcher and send events through. This is where mocking application services comes in. We're writing an integration test, so we care about interacting with code we don't own. Code we do own has tests elsewhere. The only thing we should mock in our tests is the PostRepository. Everything else, we can (and should) use real objects.

The advantage here is that we're still going to be testing the behavior of our object, but the integration with real components gives us confidence that the listener really works as expected.

Here's the outline of our tests. There's some helper methods in there to make our tests more intention revealing and concise.

<?php
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;

class CurrentPostListenerTest extends \PHPUnit_Framework_TestCase
{
    const POSTID = 123;

    private $posts, $listener, $dispatcher, $request;

    protected function setUp()
    {
        $this->posts = $this->getMock(PostRepository::class);
        $this->listener = new CurrentPostListener($this->posts);
        $this->dispatcher = new EventDispatcher();
        $this->dispatcher->addSubscriber($this->listener);
        $this->request = Request::create('/');
    }

    private function willNotFetchPost()
    {
        $this->posts->expects($this->never())
            ->method('get');
    }

    private function createPost()
    {
        return new Post(self::POSTID, 'test', 'test');
    }

    private function createEvent()
    {
        return new GetResponseEvent(
            $this->getMock(HttpKernelInterface::class),
            $this->request,
            HttpKernelInterface::MASTER_REQUEST
        );
    }
}

Now let's write our first test: if the post has already been set on the request, nothing should happen.

<?php
class CurrentPostListenerTest extends \PHPUnit_Framework_TestCase
{
    // ...

    public function testRequestAlreadySetObjectKeyDoesNotFetchPostFromDatabase()
    {
        $this->willNotFetchPost();
        $post = $this->createPost();
        $this->request->attributes->set(
            CurrentPostListener::OBJECT_KEY,
            $post
        );

        $this->dispatcher->dispatch(
            KernelEvents::REQUEST,
            $this->createEvent()
        );
    }

    // ...
}

Simple! As an exercise, try to test the same thing mocking everything. The test code will likely be a bit longer and, arguably, harder to follow.

As another example, let's test that a NotFoundHttpException is thrown when the repository can't find a post for the given ID.

<?php
class CurrentPostListenerTest extends \PHPUnit_Framework_TestCase
{
    // ...

    /**
     * @expectedException Symfony\Component\HttpKernel\Exception\NotFoundHttpException
     */
    public function testListenerThrowsNotFoundWhenNoPostIsFoundForAGivenId()
    {
        $this->setExpectedException(
            NotFoundHttpException::class
        );
        $this->posts->expects($this->once())
            ->method('get')
            ->with(self::POSTID)
            ->willReturn(null);
        $this->request->attributes->set(
            CurrentPostListener::ID_KEY,
            self::POSTID
        );

        $this->dispatcher->dispatch(
            KernelEvents::REQUEST,
            $this->createEvent()
        );
    }

    // ...
}

The rest of the tests and the real implementation of CurrentPostListener can be found here.

Takeaways

The biggest thing we gain by this style of integration testing is confidence that we're interacting with our external libraries correctly. A side effect is that it may force more and more business logic into services leaving the integraton points with the outside world to be very thin and easy to test.

#