Loading...
Loading...
Comprehensive pytest testing guide for FastAPI backends. Covers unit testing, integration testing, async patterns, mocking, fixtures, coverage, and FastAPI-specific testing with TestClient. Use when writing or updating test code for backend services, repositories, or API routes.
npx skill4agent add chacha95/advanced-harness pytest-backend-testingtests/unit/{domain}/test_{module}.py@pytest.mark.asynciotest_<what>_<when>_<expected>pytest --cov=backend --cov-report=term-missingbackend/
tests/
conftest.py # Global fixtures
unit/
domain/
artist/
test_artist_repository.py
test_artist_service.py
artwork/
auth/
...
middleware/
test_error_handler.py
utils/
test_utils.py
integration/ # End-to-end tests
test_artist_api.py
test_auth_flow.pyimport pytest
from sqlmodel.ext.asyncio.session import AsyncSession
@pytest.mark.asyncio
async def test_get_artist_by_id(db_session: AsyncSession):
# Arrange
artist_id = "test-artist-id"
# Act
result = await repository.get_by_id(artist_id)
# Assert
assert result is not None
assert result.id == artist_idfrom unittest.mock import AsyncMock, MagicMock
@pytest.mark.asyncio
async def test_create_artist_success():
# Arrange
mock_session = AsyncMock(spec=AsyncSession)
mock_session.execute = AsyncMock()
mock_session.commit = AsyncMock()
# Act
service = ArtistService(mock_session)
result = await service.create_artist(data)
# Assert
assert mock_session.commit.calledfrom fastapi.testclient import TestClient
from backend.main import create_application
@pytest.fixture
def client():
app = create_application()
return TestClient(app)
def test_get_artist_endpoint(client):
# Act
response = client.get("/api/v1/artists/test-id")
# Assert
assert response.status_code == 200
assert response.json()["id"] == "test-id"# Pattern: test_<what>_<when>_<expected>
def test_create_artist_with_valid_data_returns_artist()
def test_get_artist_when_not_found_raises_not_found_error()
def test_update_artist_with_duplicate_name_raises_conflict_error()@pytest.mark.asyncio
async def test_artist_service_create():
# Mock repository
mock_repo = AsyncMock()
mock_repo.create = AsyncMock(return_value=artist_model)
# Test service logic
service = ArtistService(mock_repo)
result = await service.create_artist(data)
assert result.name == data.name@pytest.mark.asyncio
async def test_create_artist_flow(db_session, client):
# Full flow: API → Service → Repository → DB
response = client.post("/api/v1/artists", json=artist_data)
assert response.status_code == 201
# Verify in database
artist = await db_session.get(Artist, response.json()["id"])
assert artist is not None@pytest.mark.asynciofrom unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_async_function():
mock_func = AsyncMock(return_value="result")
result = await mock_func()
assert result == "result"
mock_func.assert_awaited_once()import pytest
@pytest.fixture
def sample_artist():
return Artist(
id="test-id",
name="Test Artist",
bio="Test bio"
)
@pytest.fixture
async def db_session():
# Setup test database session
async with get_test_session() as session:
yield session
await session.rollback()# Run tests with coverage
pytest --cov=backend --cov-report=term-missing
# Generate HTML report
pytest --cov=backend --cov-report=html
# Check coverage threshold
pytest --cov=backend --cov-fail-under=80from fastapi.testclient import TestClient
def test_create_artist_endpoint(client: TestClient):
response = client.post(
"/api/v1/artists",
json={"name": "Artist", "bio": "Bio"}
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Artist"| Need to... | Read this resource |
|---|---|
| Understand test structure | testing-architecture.md |
| Write unit tests | unit-testing.md |
| Write integration tests | integration-testing.md |
| Test async code | async-testing.md |
| Use mocks and fixtures | mocking-fixtures.md |
| Improve coverage | coverage-best-practices.md |
| Test FastAPI routes | fastapi-testing.md |
"""Tests for Artist domain."""
import pytest
from unittest.mock import AsyncMock, MagicMock
from sqlmodel.ext.asyncio.session import AsyncSession
from backend.domain.artist.service import ArtistService
from backend.domain.artist.repository import ArtistRepository
from backend.domain.artist.model import Artist
from backend.dtos.artist import ArtistRequestDto
from backend.error import NotFoundError
@pytest.fixture
def sample_artist():
"""Fixture for sample artist data."""
return Artist(
id="test-artist-id",
name="Test Artist",
bio="Test bio"
)
@pytest.fixture
def mock_session():
"""Fixture for mocked database session."""
return AsyncMock(spec=AsyncSession)
class TestArtistRepository:
"""Test suite for ArtistRepository."""
@pytest.mark.asyncio
async def test_get_by_id_success(self, mock_session, sample_artist):
"""Test get_by_id returns artist when found."""
# Arrange
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = sample_artist
mock_session.execute = AsyncMock(return_value=mock_result)
repository = ArtistRepository(mock_session)
# Act
result = await repository.get_by_id("test-artist-id")
# Assert
assert result is not None
assert result.id == sample_artist.id
assert result.name == sample_artist.name
@pytest.mark.asyncio
async def test_get_by_id_not_found(self, mock_session):
"""Test get_by_id returns None when not found."""
# Arrange
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_session.execute = AsyncMock(return_value=mock_result)
repository = ArtistRepository(mock_session)
# Act
result = await repository.get_by_id("nonexistent-id")
# Assert
assert result is None
class TestArtistService:
"""Test suite for ArtistService."""
@pytest.mark.asyncio
async def test_create_artist_success(self, mock_session, sample_artist):
"""Test create_artist creates and returns artist."""
# Arrange
mock_repo = AsyncMock()
mock_repo.create = AsyncMock(return_value=sample_artist)
service = ArtistService(mock_session)
service._repository = mock_repo
request_dto = ArtistRequestDto(
name="Test Artist",
bio="Test bio"
)
# Act
result = await service.create_artist(request_dto)
# Assert
assert result.name == request_dto.name
mock_repo.create.assert_awaited_once()[tool.pytest.ini_options]
testpaths = ["tests"]
filterwarnings = ["ignore::DeprecationWarning"]
markers = [
"security: marks tests as security tests (SQL injection, etc.)",
"performance: marks tests as performance benchmarks",
]
addopts = [
"--cov=backend",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-fail-under=80",
]tests/*__init__.pybackend/main.py