← назад к разделу

Любой код рано или поздно ломается — вопрос только в том, узнаете вы об этом из теста или от пользователя. 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 — работа с базой данных и миграции для тестовой базы.