Testing Guide
This guide covers testing standards and practices for the Serko Northsky project.
Testing Framework
The project uses pytest with async support:
# Run all tests
poetry run pytest
# Run specific test file
poetry run pytest tests/serko_northsky/core/services/user_service_test.py
# Run with coverage
poetry run pytest --cov=serko_northsky
# Run with verbose output
poetry run pytest -v
Test Structure
Tests mirror the source directory structure:
tests/
├── conftest.py # Shared fixtures
└── serko_northsky/
├── api/
│ └── v1/
│ └── routers/
│ └── user_router_test.py
├── core/
│ └── services/
│ └── user_service_test.py
└── infra/
└── repositories/
└── user_repository_test.py
Naming Conventions
Test Files
- Suffix:
_test.py - Example:
user_service_test.py
Test Classes
- Prefix:
Test - Example:
TestUserService
Test Methods
Format: test_<method>__<when_condition>__<expected_result>
class TestUserService:
async def test_create__when_email_valid__returns_user(self):
...
async def test_create__when_email_exists__raises_duplicate_error(self):
...
async def test_get_by_id__when_user_not_found__returns_none(self):
...
Fixtures
Using conftest.py
Check existing fixtures before creating new ones:
# tests/conftest.py - Shared fixtures
@pytest.fixture
def mock_uow() -> AsyncMock:
"""Mock Unit of Work for service tests."""
uow = AsyncMock(spec=BaseUnitOfWork)
uow.users = AsyncMock(spec=UserRepository)
uow.save_changes = AsyncMock()
return uow
@pytest.fixture
def user_service(mock_uow: AsyncMock) -> UserService:
"""User service with mocked UoW."""
return UserService(mock_uow)
Fixture Scope
# Function scope (default) - recreated for each test
@pytest.fixture
def user():
return User(email="test@example.com")
# Module scope - shared across tests in module
@pytest.fixture(scope="module")
def db_connection():
return create_test_connection()
# Session scope - shared across entire test session
@pytest.fixture(scope="session")
def app():
return create_test_app()
Writing Tests
Service Tests
import pytest
from unittest.mock import AsyncMock
from uuid import uuid4
from serko_northsky.core.services.user_service import UserService
class TestUserService:
@pytest.fixture
def mock_uow(self) -> AsyncMock:
uow = AsyncMock()
uow.users = AsyncMock()
uow.save_changes = AsyncMock()
return uow
@pytest.fixture
def service(self, mock_uow: AsyncMock) -> UserService:
return UserService(mock_uow)
async def test_create__when_email_valid__returns_user(
self,
service: UserService,
mock_uow: AsyncMock,
):
# Arrange
email = "test@example.com"
expected_user = User(id=uuid4(), email=email)
mock_uow.users.get_by_field.return_value = None
mock_uow.users.create.return_value = expected_user
# Act
result = await service.create(email=email, auth_id="auth123")
# Assert
assert result.email == email
mock_uow.users.create.assert_called_once()
mock_uow.save_changes.assert_called_once()
async def test_create__when_email_exists__raises_error(
self,
service: UserService,
mock_uow: AsyncMock,
):
# Arrange
existing_user = User(id=uuid4(), email="test@example.com")
mock_uow.users.get_by_field.return_value = existing_user
# Act & Assert
with pytest.raises(DuplicateEmailError):
await service.create(email="test@example.com", auth_id="auth123")
Repository Tests
class TestUserRepository:
@pytest.fixture
def session(self) -> AsyncMock:
return AsyncMock(spec=AsyncSession)
@pytest.fixture
def repository(self, session: AsyncMock) -> UserRepository:
return UserRepository(session)
async def test_get_by_id__when_user_exists__returns_user(
self,
repository: UserRepository,
session: AsyncMock,
):
# Arrange
user_id = uuid4()
expected_user = User(id=user_id, email="test@example.com")
session.get.return_value = expected_user
# Act
result = await repository.get_by_id(user_id)
# Assert
assert result == expected_user
session.get.assert_called_once_with(User, user_id)
Router Tests
from httpx import AsyncClient
from fastapi import status
class TestUserRouter:
@pytest.fixture
def mock_user_service(self) -> AsyncMock:
return AsyncMock(spec=BaseUserService)
async def test_get_profile__when_authenticated__returns_profile(
self,
client: AsyncClient,
mock_user_service: AsyncMock,
auth_token: str,
):
# Arrange
expected_user = User(id=uuid4(), email="test@example.com")
mock_user_service.get_user_profile_with_relations.return_value = expected_user
# Act
response = await client.get(
"/api/users/profile",
headers={"Authorization": f"Bearer {auth_token}"},
)
# Assert
assert response.status_code == status.HTTP_200_OK
assert response.json()["email"] == "test@example.com"
Avoiding Magic Numbers
# ❌ Bad: Magic numbers
async def test_create__returns_correct_count(self):
result = await service.create_batch(items)
assert len(result) == 5
# ✅ Good: Computed from fixtures
async def test_create__returns_correct_count(self, batch_items: list[Item]):
result = await service.create_batch(batch_items)
assert len(result) == len(batch_items)
# ✅ Good: Self-documenting constant
EXPECTED_BATCH_SIZE = 5
async def test_create__returns_correct_count(self):
items = create_items(EXPECTED_BATCH_SIZE)
result = await service.create_batch(items)
assert len(result) == EXPECTED_BATCH_SIZE
Async Testing
Use pytest-asyncio for async tests:
import pytest
# Mark individual test
@pytest.mark.asyncio
async def test_async_function():
result = await async_function()
assert result is not None
# Mark entire class
@pytest.mark.asyncio
class TestAsyncService:
async def test_method(self):
...
Mocking
AsyncMock for Async Methods
from unittest.mock import AsyncMock, patch
async def test_with_mock():
mock = AsyncMock(return_value="result")
result = await mock()
assert result == "result"
Patching
@patch("serko_northsky.core.services.user_service.send_email")
async def test_create_sends_email(self, mock_send_email: AsyncMock):
await service.create(email="test@example.com")
mock_send_email.assert_called_once()
Test Coverage
Maintain high test coverage:
# Generate coverage report
poetry run pytest --cov=serko_northsky --cov-report=html
# Check coverage threshold
poetry run pytest --cov=serko_northsky --cov-fail-under=80
Related Documentation
- Code Review — Review guidelines
- Backend Architecture — Architecture patterns