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¶
UserRepositoryis the port. The SQL adapter and the fake both implement it, andUserServicedepends on the port only.InMemoryUserRepositoryis 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:
registerandget_by_email, not the dict of the fake.
What to adapt¶
User,UserRepositoryandUserServiceare 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¶
test-in-memory-adapters(BTTEST001)- CES-64