Skip to content

In-memory adapters for tests

A test drives the logic of a service through a fake that implements the same Protocol as the production adapter.

What to read

  • UserRepository is the port. The SQL adapter and the fake both implement it, and UserService depends on the port only.
  • InMemoryUserRepository is the fake. It implements the whole port with a dict, so a test runs without I/O.
  • The tests give the fake to the service and assert on the result through the interface: register and get_by_email, not the dict of the fake.

What to adapt

  • User, UserRepository and UserService are one example. Write the Protocol of the port that your service depends on, and a fake that implements all of it.
  • Put the fake beside your tests, in tests/fakes/.

References

code/app/core/users.py:17-30

the port: the Protocol that the SQL adapter and the fake both implement

class UserRepository(Protocol):
    """The port. The SQL adapter and the fake of the tests both implement it."""

    def add(self, user: User) -> None:
        """Store a user."""
        ...

    def get_by_email(self, email: str) -> User | None:
        """Return the user with this email, or None."""
        ...

    def next_id(self) -> int:
        """Return an id that no stored user has."""
        ...

code/tests/fakes/users.py

the fake: the whole port, kept in a dict, with no I/O

"""A fake of the repository of users: the whole port, kept in a dict, with no I/O."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from app.core.users import User


@dataclass
class InMemoryUserRepository:
    """The same interface as the SQL repository, kept in memory."""

    _by_email: dict[str, User] = field(default_factory=dict)

    def add(self, user: User) -> None:
        """Store a user."""
        self._by_email[user.email] = user

    def get_by_email(self, email: str) -> User | None:
        """Return the user with this email, or None."""
        return self._by_email.get(email)

    def next_id(self) -> int:
        """Return an id that no stored user has."""
        return len(self._by_email) + 1

code/tests/test_users.py

tests that give the fake to the service and assert through the public interface

"""The service of users, driven through the fake of its repository."""

from __future__ import annotations

import pytest
from tests.fakes.users import InMemoryUserRepository

from app.core.users import EmailTakenError, UserService


def test_register_stores_the_user_with_its_email_in_lower_case():
    users = InMemoryUserRepository()

    user = UserService(users).register("Ana@Example.com")

    assert users.get_by_email("ana@example.com") == user


def test_an_email_is_registered_once():
    service = UserService(InMemoryUserRepository())
    service.register("ana@example.com")

    with pytest.raises(EmailTakenError):
        service.register("ANA@example.com")

Cited by