Тестирование FastAPI-приложений

FastAPI тестируют через TestClient (поверх httpx), который шлёт запросы приложению без реального сервера, а зависимости подменяют через app.dependency_overrides для изоляции от БД и внешних сервисов.

Тест в FastAPI — это обычный вызов эндпоинта в памяти: TestClient делает «запрос» прямо к приложению и возвращает ответ, как настоящий клиент, но без сети и без uvicorn.

Тестируемость — следствие архитектуры FastAPI. Поскольку всё внешнее (БД, токены, сервисы) приходит через зависимости, в тесте их легко заменить на подделки. А TestClient позволяет вызывать эндпоинты как HTTP, не поднимая сервер: он гоняет запрос через всё приложение (валидацию, зависимости, обработчик) и отдаёт ответ для проверок.

from fastapi.testclient import TestClient
from main import app, get_db

client = TestClient(app)

def fake_db():
    return {"users": {1: {"id": 1, "name": "Тест"}}}

app.dependency_overrides[get_db] = fake_db   # подменяем реальную БД

def test_get_user():
    response = client.get("/users/1")
    assert response.status_code == 200
    assert response.json()["name"] == "Тест"

def test_user_not_found():
    response = client.get("/users/999")
    assert response.status_code == 404

Ключевая техника — dependency_overrides: словарь, где реальной зависимости сопоставляется тестовая. Эндпоинт ничего не знает о подмене — он по-прежнему просит get_db, но получает фальшивую. Так тесты не ходят в настоящую базу, выполняются быстро и детерминированно. После теста подмену сбрасывают.

Как работает под капотом

Переопределение зависимостей — это просто подмена функции в словаре до разрешения графа. FastAPI при разрешении сперва смотрит в dependency_overrides. Смоделируем механизм на stdlib:

def get_db():
    return "РЕАЛЬНАЯ база (медленно, с сетью)"

overrides = {}

def resolve(dep):
    func = overrides.get(dep, dep)   # сначала ищем подмену
    return func()

def handler():
    db = resolve(get_db)
    return f"обработчик работает с: {db}"

print("без подмены:", handler())

# тест подменяет зависимость
overrides[get_db] = lambda: "ФЕЙКОВАЯ база (в памяти, быстро)"
print("в тесте:     ", handler())

overrides.clear()                    # сброс после теста
print("после сброса:", handler())

Попробуй сам ▶ Один и тот же обработчик работает то с реальной, то с фейковой зависимостью — без единого изменения в его коде. Это и есть сила DI для тестов.

Частые ошибки

Первая — ходить в настоящую базу из тестов, делая их медленными и хрупкими; подменяйте сессию. Вторая — забыть очистить dependency_overrides между тестами, из-за чего подмена «протекает» в другие тесты. Третья — тестировать только успешные пути, игнорируя ошибки (404, 422, 401). Четвёртая — путать TestClient (синхронный, для большинства тестов) и асинхронный httpx-клиент (нужен для проверки настоящего async-поведения).

Best practices

  • Подменяйте внешние зависимости (БД, токены) через dependency_overrides ради скорости и детерминизма.
  • Очищайте переопределения между тестами (например, в фикстуре pytest).
  • Покрывайте и ошибочные ветки: 404, 422, 401, а не только успех.
  • Используйте отдельную тестовую БД/транзакции с откатом, если нужна реальная интеграция.

Пирамида тестов и фикстуры pytest

Не все тесты одинаково ценны. Полезно держать в голове пирамиду: много быстрых модульных тестов на чистую логику (валидаторы, сервисы) в основании, меньше интеграционных тестов эндпоинтов через TestClient в середине, и совсем немного сквозных тестов с реальной базой наверху. TestClient-тесты с подменой зависимостей — золотая середина: они проверяют реальный конвейер (валидацию, зависимости, обработчик), оставаясь быстрыми. Организуют их через фикстуры pytest: одна фикстура создаёт клиента, другая настраивает и сбрасывает dependency_overrides, третья выдаёт тестовую сессию БД с откатом транзакции после каждого теста, чтобы тесты не влияли друг на друга. Такой подход даёт детерминированный, изолированный и быстрый набор тестов. Помните: тест без проверки ошибочных веток — половина теста; всегда проверяйте, что на плохой вход API отвечает 422, на отсутствие ресурса — 404, на чужой токен — 401.

Итог: TestClient гоняет запросы через всё приложение без сервера, а dependency_overrides подменяет внешние зависимости фейками. Архитектура на DI делает FastAPI тестируемым по своей природе — пользуйтесь этим и проверяйте не только счастливые пути.

Проверьте себя
1. Что делает app.dependency_overrides[get_db] = fake_db в тесте?
AУдаляет эндпоинты, использующие get_db
BПодменяет реальную зависимость get_db фейковой fake_db, не меняя код обработчиков
CУскоряет реальную базу
DОтключает валидацию
2. Почему TestClient удобен для тестов?
AОн поднимает реальный uvicorn-сервер на отдельном порту
BОн гоняет запросы через всё приложение (валидацию, зависимости, обработчик) в памяти, без реальной сети и сервера
CОн тестирует только синтаксис
DОн работает только с базой данных