Гайды

Тестирование FastAPI: pytest, TestClient и AsyncClient

httpx AsyncClient, фикстуры, dependency_overrides, БД в тестах, параметризация и покрытие в CI.

~10 мин чтения

Тестирование FastAPI: pytest, TestClient и AsyncClient

Тесты API на FastAPI строят на httpx (рекомендуемый AsyncClient) или классическом TestClient из Starlette. pytest даёт фикстуры для приложения и переиспользуемой БД. Каркас приложения — Быстрый старт с FastAPI.


1. Зависимости

bash
pip install pytest pytest-asyncio httpx

В pytest.ini или pyproject.toml:

ini
[pytest]
asyncio_mode = auto

2. Синхронный TestClient

python
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_health():
    r = client.get("/health")
    assert r.status_code == 200
    assert r.json() == {"status": "ok"}

Подходит для sync эндпоинтов и простых случаев; внутри поднимает ASGI-сервер в потоке.


3. AsyncClient (предпочтительно для async)

python
import pytest
from httpx import ASGITransport, AsyncClient
from main import app

@pytest.fixture
async def ac():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        yield c

@pytest.mark.asyncio
async def test_items(ac: AsyncClient):
    r = await ac.get("/items/1")
    assert r.status_code == 200

Так вы избегаете смешивания async/sync в одном цикле событий.


4. Переопределение Depends (мок БД / пользователя)

python
from fastapi import Depends

async def fake_user():
    return {"id": 1, "role": "admin"}

app.dependency_overrides[get_current_user] = fake_user

try:
    r = client.get("/admin/stats")
finally:
    app.dependency_overrides.clear()

Или в фикстуре yield — очищайте overrides после теста.


5. Фикстура БД

  • SQLite in-memory + aiosqlite для интеграционных тестов.
  • Или testcontainers с PostgreSQL для максимальной близости к продакшену.

Прокиньте URL БД через os.environ до импорта settings.


6. Параметризация

python
@pytest.mark.parametrize("item_id,status", [(1, 200), (-1, 404)])
def test_get_item(item_id, status):
    r = client.get(f"/items/{item_id}")
    assert r.status_code == status

7. Покрытие и CI

bash
pytest -q --cov=app --cov-report=term-missing

В CI поднимайте только unit/integration без внешних сервисов или с контейнерами.


8. Что ещё тестировать

  • 422 на невалидном теле (Pydantic).
  • 401/403 на защищённых маршрутах — см. JWT и OAuth2.
  • Заголовки Location для 201/303.

9. Lifespan и фоновые задачи

Если приложение использует @asynccontextmanager lifespan (пулы БД, клиенты), поднимайте AsyncClient внутри тестовой фикстуры после инициализации lifespan или используйте LifespanManager из библиотеки asgi-lifespan, чтобы в тесте отработали те же startup/shutdown хуки, что в проде.


10. Фикстуры и изоляция

Держите TestClient/AsyncClient на scope function, чтобы dependency_overrides не протекали между тестами. Для тяжёлой БД — session-scoped контейнер Testcontainers + transaction rollback на каждый тест (быстрее, чем пересоздание схемы). Генерация данных — Factory Boy / faker вместо ручных JSON в десятках тестов.


11. WebSocket (smoke)

У TestClient есть контекстный менеджер websocket_connect — проверка рукопожатия и первого кадра без отдельного браузера:

python
with client.websocket_connect("/ws/notify") as ws:
    ws.send_json({"type": "ping"})
    data = ws.receive_json()
    assert data.get("type") == "pong"

Для AsyncClient смотрите актуальный API httpx / обёртки; сложные сценарии часто выносят в e2e с реальным ASGI-сервером.


Дальше: Pydantic · тег pytest