DEV Community

Daniel Rotter
Daniel Rotter

Posted on • Originally published at danielrotter.at

Avoid mocking repositories by using in-memory implementations

One of the most important aspects of testing - besides finding errors in an application - is how long it takes to run
them. If tests for an application take minutes or even hours to finish, then they are not suitable for developing using
a fast feedback loop and developers might not run them as often as they should.

The testing pyramid has many goals, and one of them is to have a fast
test suite so that developers do not have to wait too long for their tests to finish. It does so by introducing three
different kinds of tests: UI, service, and unit. The basic idea is that unit tests are the fastest to run, and
therefore most of the testing should be implemented as unit tests.

Testing does not come with clear definitions for all of its terms, so I want to clarify that lately, I like to use
sociable unit tests over solitary ones. They make me much more
confident since a real implementation is used for the dependencies of a unit. However, if not used carefully they
might be very slow
.

Solitary unit tests will always mock dependencies, which makes them fast since all dependencies of a unit are
replaced with a mock implementation
. Very often some kind of library or framework is used for that, e.g. test doubles
from PHPUnit
or a separate mocking library like
Prophecy or Mockery. While they can make
tests fast by setting up expectations and the desired return value, especially if used for slow parts like code
connecting to a database, they come with some serious issues:

  • Mocks can easily hide actual errors because they are still returning "old" values if the behavior of an implementation changes for some reason.
  • Mocks are often defined in multiple tests in a similar way, making them awkward to use compared to a "real" implementation.
  • Mocks are tightly coupled to the real implementation making refactorings even harder since a change might cause necessary changes in many tests as well.
  • Mocks are not very straightforward to define and make the test code harder to read, although that might be subjective.
  • Mocking libraries often use dynamic classes, which makes understanding and debugging them quite hard. When stepping into a mocked function call in a debug session there is no straightforward code. Instead, you might land in a file with hundreds of lines of non-trivial code, or - even worse - in a dynamically created file not even existing in the file system.

At the beginning of my career, I was not aware of these issues and used solitary unit tests with loads of mocks. We
often did refactorings, which did not make tests fail although the code was not working in production and I have spent
quite some hours debugging third-party code.

Fortunately, there is another method of making tests fast and have more reliable tests at the same time: Define a
single interface, write an abstract test against that interface, and have the same tests run against one implementation
for production and a much faster implementation for tests.
This will solve multiple of the issues above:

  • Errors are less likely to be hidden because both implementations should behave the same since they run against the same tests.
  • The test implementation can be reused in every test instead of setting up mocks every time.
  • The tests are using a simple class instead of a complex mocking library.
  • Debuggers will land in a real class developers know, instead of something dynamically generated.

The rest of the blog post will explain how this can be done in Symfony, but the general principles should apply to any
framework and programming language. The example code can also be found as a working application in a GitHub
repository
.

Define a common interface

The example will implement two different repositories, one using the Doctrine
ORM
for use in production and an in-memory implementation using an
array to store objects. I will use a generic Item class to keep things generic:

<?php

namespace App\Domain;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Symfony\Component\Uid\Uuid;

#[Entity]
class Item
{
    #[Id]
    #[Column(type: 'uuid')]
    private Uuid $id;

    public function __construct(
        #[Column] private string $title,
        #[Column] private string $description,
    ) {
        $this->id = Uuid::v4();
    }

    public function getId(): Uuid
    {
        return $this->id;
    }

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

    public function getDescription(): string
    {
        return $this->description;
    }
}

Enter fullscreen mode Exit fullscreen mode

Good domain objects would contain more methods than just getters, but for the sake of brevity, I will keep it like that
for this blog post.

This is more or less the simplest Doctrine entity that can be created, it only contains a Uuid as an identifier and a
field for a title and a description. Additionally, the domain layer introduces an interface for an ItemRepository,
which takes care of persisting and retrieving objects from data storage:

<?php

namespace App\Domain;

interface ItemRepositoryInterface
{
    public function add(Item $item): void;

    /**
     * @return Item[]
     */
    public function loadAll(): array;

    /**
     * @return Item[]
     */
    public function loadFilteredByTitle(string $titleFilter): array;
}

Enter fullscreen mode Exit fullscreen mode

The contract defined in this interface allows the application to not care about which kind of storage is used, and
therefore most tests can use a much faster one than a relational database.
However, in order to swap out
implementations reliably it must be ensured that all of them behave in the same way. That is where the abstract test
case comes in.

Implement the abstract test case

As mentioned previously, the abstract test class is responsible for ensuring that all implementations of the
ItemRepositoryInterface behave in the same way. One characteristic of repositories is that adding the same object
twice will result in having the object only once in the repository. So let's test that and adding two different objects
to the repository as well as filtering items by their title. Since currently the ItemRepository interface only has
three methods this covers all of its functionality already.

<?php

namespace App\Tests\Repository;

use App\Domain\Item;
use App\Domain\ItemRepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

abstract class AbstractItemRepositoryTest extends KernelTestCase
{
    abstract protected function createItemRepository(): ItemRepositoryInterface;

    abstract protected function flush(): void;

    public function testMultipleAddOfItem(): void
    {
        $itemRepository = $this->createItemRepository();

        $item = new Item('Test title', 'Test description');

        $itemRepository->add($item);
        $itemRepository->add($item);

        $this->flush();

        $items = $itemRepository->loadAll();

        $this->assertCount(1, $items);
        $this->assertContains($item, $items);
    }

    public function testLoadAllWithMultipleItems(): void
    {
        $itemRepository = $this->createItemRepository();

        $item1 = new Item('Test title 1', 'Test description 1');
        $item2 = new Item('Test title 2', 'Test description 2');

        $itemRepository->add($item1);
        $itemRepository->add($item2);

        $this->flush();

        $items = $itemRepository->loadAll();

        $this->assertCount(2, $items);
        $this->assertContains($item1, $items);
        $this->assertContains($item2, $items);
    }

    public function testLoadFilteredByTitle(): void
    {
        $itemRepository = $this->createItemRepository();

        $item1 = new Item('Test title 1', 'Test description 1');
        $item2 = new Item('Title 2', 'Description 2');
        $item3 = new Item('Test title 3', 'Test description 2');

        $itemRepository->add($item1);
        $itemRepository->add($item2);
        $itemRepository->add($item3);

        $this->flush();

        $items = $itemRepository->loadFilteredByTitle('Test title');

        $this->assertCount(2, $items);
        $this->assertContains($item1, $items);
        $this->assertContains($item3, $items);
    }
}
Enter fullscreen mode Exit fullscreen mode

The test class needs to extend from the KernelTestCase of Symfony to allow getting a reference to the
EntityManagerInterface of Doctrine, which enables testing against the real database for the Doctrine repository later.

Also, two abstract methods need to be overridden by the tests for the concrete applications:

  • createItemRepository is a template method allowing to swap out the implementation for the tests.
  • flush is used to actually send changes to the database, which is necessary for the Doctrine repository later, unless you want to add the flush call to the repository itself (which I would not recommend, since a single request should have all of its changes or none being committed to the database).

With that abstract test case in place, the concrete implementations can be implemented and tested against the same set
of tests.

Write the production and testing implementation

The concrete implementations of these tests will override the createMatchRequest and flush methods. Therefore the
test for the Doctrine implementation looks like this:

<?php

namespace App\Tests\Repository\Doctrine;

use App\Domain\ItemRepositoryInterface;
use App\Repository\Doctrine\ItemRepository;
use App\Tests\Repository\AbstractItemRepositoryTest;
use Doctrine\ORM\EntityManagerInterface;

class ItemRepositoryTest extends AbstractItemRepositoryTest
{
    protected function createItemRepository(): ItemRepositoryInterface
    {
        return new ItemRepository($this->getContainer()->get(EntityManagerInterface::class));
    }

    protected function flush(): void
    {
        $this->getContainer()->get(EntityManagerInterface::class)->flush();
    }

    protected function setUp(): void
    {
        $this->getContainer()->get(EntityManagerInterface::class)->getConnection()->setNestTransactionsWithSavepoints(true);
        $this->getContainer()->get(EntityManagerInterface::class)->getConnection()->beginTransaction();
    }

    protected function tearDown(): void
    {
        $this->getContainer()->get(EntityManagerInterface::class)->getConnection()->rollBack();
    }
}
Enter fullscreen mode Exit fullscreen mode

In here the createItemRepository will return an instance of App\Repository\Doctrine\ItemRepository, which also
requires an instance of the EntityManagerInterface to work properly since it uses this class to store and retrieve
data from the database. The flush method will call flush on the EntityManagerInterface, which results in the data
actually being stored (this is called in the abstract test case). Additionally, the setUp and tearDown methods will
ensure that each test is enclosed in a transaction by calling beginTransaction and rollBack. This way no data is
actually stored in the database, which makes the tests very fast
. However, be careful, since there might still be
database checks that could fail at this point. Last but not least the setNestTransactionWithSavepoints method is
necessary to allow nesting transactions.

The following ItemRepository implementation will make use of the EntityManagerInterface and fulfill the previously
shown tests:

<?php

namespace App\Repository\Doctrine;

use App\Domain\Item;
use App\Domain\ItemRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;

class ItemRepository implements ItemRepositoryInterface
{
    public function __construct(private EntityManagerInterface $entityManager)
    {

    }

    public function add(Item $item): void
    {
        $this->entityManager->persist($item);
    }

    public function loadAll(): array
    {
        /** @var Item[] */
        return $this->entityManager
            ->createQueryBuilder()
            ->from(Item::class, 'i')
            ->select('i')
            ->getQuery()
            ->getResult()
        ;
    }

    public function loadFilteredByTitle(string $titleFilter): array
    {
        /** @var Item[] */
        return $this->entityManager
            ->createQueryBuilder()
            ->from(Item::class, 'i')
            ->select('i')
            ->where('i.title LIKE :titleFilter')
            ->setParameter('titleFilter', $titleFilter . '%')
            ->getQuery()
            ->getResult()
        ;
    }
}
Enter fullscreen mode Exit fullscreen mode

The tests for the memory implementation are a bit simpler since there is no dependency like the EntityManagerInterface
and there is also no need to call a method like flush. Therefore createItemRepository will just return a new
instance and the flush method can be left empty:

<?php

namespace App\Tests\Repository\Memory;

use App\Domain\ItemRepositoryInterface;
use App\Repository\Memory\ItemRepository;
use App\Tests\Repository\AbstractItemRepositoryTest;

class ItemRepositoryTest extends AbstractItemRepositoryTest
{
    protected function createItemRepository(): ItemRepositoryInterface
    {
        return new ItemRepository();
    }

    protected function flush(): void
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

The implementation fulfilling these tests uses a simple array containing the objects, which only needs to check if the
array already contains the passed Item to avoid inserting it multiple times:

<?php

namespace App\Repository\Memory;

use App\Domain\Item;
use App\Domain\ItemRepositoryInterface;

class ItemRepository implements ItemRepositoryInterface
{
    /**
     * @var Item[]
     */
    private array $items = [];

    public function add(Item $item): void
    {
        if (in_array($item, $this->items)) {
            return;
        }

        $this->items[] = $item;
    }

    public function loadAll(): array
    {
        return $this->items;
    }

    public function loadFilteredByTitle(string $titleFilter): array
    {
        return array_values(
            array_filter(
                $this->items,
                fn (Item $item) => str_contains($item->getTitle(), $titleFilter),
            ),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The only bit that is a bit cumbersome here is the loadFilteredByTitle method, since this method will only be
implemented for the tests, which would not be necessary if mocks were used.
But therefore mocks might lead to wrong
test results if the behavior of this method changes for some reason. In this example array_filter was used to return
only the items matching the given criteria, but it would also be possible to use a foreach loop or whatever else works
for you. Of course this is still a very simple example and depending on the actual logic this might be harder to
implement, but I would not consider this wasted effort since it gives me confidence and fast tests.

This implementation cannot be used in a production environment unless you want every request to start with no data at
all.
However, other than that, this implementation behaves exactly the same as Doctrine one actually storing data in
the database. This makes it a great candidate to use for other tests, many of which mocks would probably be used
otherwise.

Use the correct implementation in each environment

So now that we have two implementations of the same interface we can use them interchangeably in e.g. a REST Controller
like shown in the following code:

<?php

namespace App\Controller;

use App\Domain\Item;
use App\Domain\ItemRepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class ItemController extends AbstractController
{
    #[Route('/items', methods: ['GET'])]
    public function list(Request $request, ItemRepositoryInterface $itemRepository): JsonResponse
    {
        $titleFilter = $request->query->getString('titleFilter');
        $items = $titleFilter ? $itemRepository->loadFilteredByTitle($titleFilter) : $itemRepository->loadAll();

        return $this->json($items);
    }

    #[Route('/items', methods: ['POST'])]
    public function create(Request $request, ItemRepositoryInterface $itemRepository): JsonResponse
    {
        /** @var \stdClass */
        $data = json_decode($request->getContent());
        $item = new Item($data->title, $data->description);

        $itemRepository->add($item);

        return $this->json($item);
    }
}
Enter fullscreen mode Exit fullscreen mode

This is a pretty standard Symfony controller using the ItemRepositoryInterface to inject one of the above
implementations. Symfony comes with autowiring
these days so that usually it is not necessary to configure anything. However, since we have two implementations of the
ItemRepositoryInterface Symfony cannot know which one to use. Therefore we have to add the following line to the
config/services.yaml file:

services:
    # other stuff...
    App\Domain\ItemRepositoryInterface: '@App\Repository\Doctrine\ItemRepository'
Enter fullscreen mode Exit fullscreen mode

This way Symfony knows that it should inject the Doctrine ItemRepository whenever the ItemRepositoryInterface is
used.

Mind that the controller does not call the EntityManagerInterface::flush method. I like to avoid using such methods in
the controller, since depending on which ItemRepositoryInterface is being used it might not be necessary. However, in
the case of the Doctrine implementation this must be done, therefore I started to implement a listener for that:

<?php

namespace App\Repository\Doctrine;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;

class FlushEventSubscriber implements EventSubscriberInterface
{
    public function __construct(private EntityManagerInterface $entityManager)
    {

    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::RESPONSE => ['flush'],
        ];
    }

    public function flush(): void
    {
        $this->entityManager->flush();
    }
}
Enter fullscreen mode Exit fullscreen mode

I haven't tested it, but my guess is, that the flush method should not take a long time in case no entity has been
changed.
An alternative approach would be to introduce another FlushInterface or something similar, that can also be
exchanged based on the used repository implementation.

The test for this controller can now be implemented something like this:

<?php

namespace App\Tests\Controller;

use App\Domain\Item;
use App\Domain\ItemRepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ItemControllerTest extends WebTestCase
{
    public function testList(): void
    {
        $client = static::createClient();

        /** @var ItemRepositoryInterface */
        $itemRepository = $client->getContainer()->get(ItemRepositoryInterface::class);

        $itemRepository->add(new Item('Title 1', 'Description 1'));
        $itemRepository->add(new Item('Title 2', 'Description 2'));

        $client->request('GET', '/items');

        $responseContent = $client->getResponse()->getContent();
        $this->assertNotFalse($responseContent);
        $responseData = json_decode($responseContent);

        $this->assertIsArray($responseData);
        $this->assertCount(2, $responseData);
        $this->assertEquals('Title 1', $responseData[0]->title);
        $this->assertEquals('Description 1', $responseData[0]->description);
        $this->assertEquals('Title 2', $responseData[1]->title);
        $this->assertEquals('Description 2', $responseData[1]->description);
    }

    public function testListWithTitleFilter(): void
    {
        $client = static::createClient();

        /** @var ItemRepositoryInterface */
        $itemRepository = $client->getContainer()->get(ItemRepositoryInterface::class);

        $itemRepository->add(new Item('Test title 1', 'Description 1'));
        $itemRepository->add(new Item('Title 2', 'Description 2'));
        $itemRepository->add(new Item('Test title 3', 'Description 3'));

        $client->request('GET', '/items?titleFilter=Test title');

        $responseContent = $client->getResponse()->getContent();
        $this->assertNotFalse($responseContent);
        $responseData = json_decode($responseContent);

        $this->assertIsArray($responseData);
        $this->assertCount(2, $responseData);
        $this->assertEquals('Test title 1', $responseData[0]->title);
        $this->assertEquals('Description 1', $responseData[0]->description);
        $this->assertEquals('Test title 3', $responseData[1]->title);
        $this->assertEquals('Description 3', $responseData[1]->description);
    }

    public function testCreate(): void
    {
        $client = static::createClient();

        /** @var ItemRepositoryInterface */
        $itemRepository = $client->getContainer()->get(ItemRepositoryInterface::class);

        $client->jsonRequest('POST', '/items', ['title' => 'Title', 'description' => 'Description']);

        $items = $itemRepository->loadAll();
        $this->assertCount(1, $items);
        $this->assertEquals('Title', $items[0]->getTitle());
        $this->assertEquals('Description', $items[0]->getDescription());
    }
}
Enter fullscreen mode Exit fullscreen mode

I will not go into every detail of testing in Symfony (the Symfony testing
documentation
already does a decent job at this), instead, I will only
talk about the highlight: This test relies on the ItemRepositoryInterface instead of the Doctrine one. It is used
to setup some data in the testList and testListWithTitleFilter tests and also to assert if data was actually stored
testCreate. If the tests are run like this they will not often succeed, since the database is never reset. However,
the goal of this blog post is not to use databases for this kind of test anyway. Therefore a config/services_test.yaml
file is created instead, which contains the following lines:

services:
    App\Domain\ItemRepositoryInterface: '@App\Repository\Memory\ItemRepository'
Enter fullscreen mode Exit fullscreen mode

This way for all tests the ItemRepository using just an array as memory is used whenever the
ItemRepositoryInterface is being referred. This means that with this configuration no database at all is used in the
above test for the controller, which makes the tests incredibly fast. At the same time, these tests are quite
reliable since the memory implementation behaves like the Doctrine implementation because of the
AbstractItemRepositoryTest.

The only test actually running against the database is the ItemRepositoryTest for the Doctrine implementation, which
only injects the EntityManagerInterface, for which reason the configuration in services_test.yaml does not apply in
this case.

Conclusion

In summary, I can say that I have never been so happy with my tests. They are incredibly fast, give me a lot of
confidence since the memory implementation should behave very similar to the Doctrine implementation, and there is no
need to redefine a lot of expectations in many tests as would be the case with mocks.

The only downside I can think of is that in the case of repositories complex queries might be hard to implement using
just an array, but in my opinion, this is not a real deal breaker.
And quite often some calls to array methods like
array_filter already go a long way in this regard.

I encourage you to try this kind of testing in a project and I am sure that you will not regret it!

Top comments (0)