Любой код рано или поздно ломается — вопрос только в том, узнаете вы об этом из теста или от пользователя. FastAPI изначально проектировался так, чтобы тесты было легко писать: зависимости через Depends подменяются одной строкой, всё приложение поднимается в памяти без сети. Разберём инструменты по порядку.
TestClient: самый простой способ протестировать эндпоинт
Представьте, что вы хотите проверить: «если я отправлю GET /products/1, придёт статус 200 и нужный JSON». Для этого нужно как-то «постучать» в приложение. Поднимать настоящий сервер на порту — долго и неудобно. TestClient решает эту задачу: он поднимает приложение прямо в памяти и шлёт запросы без всякой сети.
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_get_product():
response = client.get("/products/1")
assert response.status_code == 200
assert response.json()["id"] == 1
TestClient синхронный — его можно звать из обычного def-теста, хотя само приложение асинхронное. Он сам крутит event loop внутри. Для большинства тестов эндпоинтов этого достаточно.
httpx.AsyncClient: когда тест сам должен быть асинхронным
Иногда тесту нужно не только послать запрос, но и поработать с async-кодом напрямую: например, подготовить данные через async-репозиторий или проверить что-то через async-сессию базы. Тут TestClient не подойдёт — он синхронный.
Решение: httpx.AsyncClient с ASGITransport. Он подключается к приложению напрямую, тоже без сети, но сам полностью асинхронный.
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_get_product():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/products/1")
assert response.status_code == 200
Асинхронные тесты требуют пакет pytest-asyncio и пометку @pytest.mark.asyncio (или глобальную настройку asyncio_mode = "auto" в pytest.ini).
Подмена зависимостей
Это главная суперсила тестирования во FastAPI. Допустим, эндпоинт получает сессию базы данных через Depends(get_session). В тесте не нужно трогать продакшн-базу — достаточно подменить get_session на функцию, которая даёт тестовую сессию.
async def override_get_session():
async with test_session_factory() as session:
yield session
app.dependency_overrides[get_session] = override_get_session
app.dependency_overrides — это обычный словарь: ключ — оригинальная функция-зависимость, значение — замена. Эндпоинт ничего не знает о подмене, он продолжает получать зависимость через Depends как обычно.
После теста подмену нужно убрать, иначе она повлияет на следующие тесты:
app.dependency_overrides.clear()
Фикстуры pytest
Фикстуры избавляют от повторения кода: подготовку клиента, подмену зависимостей и уборку после теста пишут один раз и переиспользуют во всех тестах.
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.db import get_session
@pytest.fixture
def client():
app.dependency_overrides[get_session] = override_get_session
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
def test_create_product(client):
response = client.post("/products/", json={"name": "Кофемолка", "price": 4990})
assert response.status_code == 201
Фикстура запускается перед каждым тестом, который её использует, и убирает за собой после — даже если тест упал. Это делает тесты независимыми друг от друга.
Тестовая база данных
Тесты, которые записывают и читают данные, должны работать с реальной базой того же типа, что и в продакшне. PostgreSQL — в PostgreSQL, а не в SQLite: диалекты расходятся, и то, что работает в SQLite, может не работать в Postgres.
Два распространённых подхода:
-
Отдельная тестовая база — перед прогоном тестов применяют миграции Alembic в чистую базу. Удобно запускать через Docker:
pytestподнимает контейнер с PostgreSQL, применяет миграции, гоняет тесты, контейнер останавливается. Библиотекаpytest-dockerилиtestcontainersпомогают автоматизировать это. -
Транзакция с откатом — каждый тест открывает транзакцию и откатывает её в конце. База остаётся чистой, тесты не видят данных друг друга. Работает быстрее, потому что данные не пишутся физически.
Поднимать базу в контейнере — стандартная практика. Так проверяется и схема, и реальные SQL-запросы, а не их имитация.
Коротко
TestClient— самый простой способ тестировать эндпоинты: поднимает приложение в памяти, работает в обычныхdef-тестах.httpx.AsyncClientсASGITransport— когда тест должен быть асинхронным; требуетpytest-asyncio.app.dependency_overrides— словарь для подмены зависимостей в тестах; после теста его нужно очищать.- Фикстуры pytest собирают окружение один раз и переиспользуют: клиент, подмены, уборка.
- Тестируйте с реальной базой того же типа (PostgreSQL, не SQLite) — через отдельную тестовую базу или транзакцию с откатом.
Что почитать дальше
- Dependency Injection во FastAPI — как работает система зависимостей, которую мы подменяем в тестах.
- Persistence: SQLAlchemy и Alembic — работа с базой данных и миграции для тестовой базы.