Внедрение зависимостей в FastAPI: границы и тестируемость
Система Depends в FastAPI выглядит просто, но на реальном проекте быстро обрастает вложенностями, скрытыми состояниями и тестами, которые ломаются без видимой причины. Разбираем, как выстроить границы зависимостей, чтобы код оставался тестируемым и не превращался в граф, который никто не может прочитать.

Почему Depends — не Spring и не Guice
FastAPI предлагает свой вариант Dependency Injection: функция или класс передаётся в Depends(), фреймворк вызывает её перед обработчиком и подставляет результат в аргумент. Никакого контейнера, никакого XML, никакой магии с аннотациями — всё на уровне обычных вызываемых объектов Python. Это сильная сторона и одновременно ловушка: простота синтаксиса маскирует архитектурные решения, которые потом дорого менять.
Материал написан для FastAPI 0.110+ и Python 3.11+. Если вы используете более раннюю версию, поведение Depends в целом совпадает, но детали кеширования и работы с async-генераторами могут отличаться — сверяйтесь с официальной документацией.
Три слоя, которые стоит разделять
Типичная ошибка — запихнуть в Depends всё подряд: и получение текущего пользователя, и открытие сессии базы данных, и бизнес-логику сервиса. Граф зависимостей растёт, и через месяц тест на один эндпоинт требует подмены пяти зависимостей, из которых три вложены друг в друга.
Практика, которая хорошо масштабируется, — разделить зависимости на три слоя:
- Инфраструктурные — сессия БД, HTTP-клиент, подключение к кешу. Они не знают о бизнес-логике, живут как генераторы с
yieldи управляют жизненным циклом ресурса. - Контекстные — текущий пользователь, права, tenant. Зависят от инфраструктурных (нужна сессия, чтобы достать пользователя), но сами логику не содержат.
- Сервисные — бизнес-операции. Принимают инфраструктурные и контекстные зависимости, но сами в
Dependsпопадать не должны. Их лучше создавать явно внутри обработчика или фабрики.
Если ваша зависимость одновременно открывает транзакцию, проверяет права и формирует ответ — это не зависимость, а замаскированный контроллер. Вынесите логику наружу, пока граф не стал нечитаемым.
Почему сервисы лучше не прятать в Depends
Когда бизнес-сервис живёт внутри Depends, у него нет явного конструктора — FastAPI сам собирает аргументы. Это удобно ровно до момента, когда нужно передать параметр, который не является зависимостью: конфиг-флаг, стратегию, лимит. Приходится либо тащить конфиг через ещё одну зависимость, либо хардкодить. Оба варианта ухудшают тестируемость.
Альтернатива — фабричная зависимость, которая возвращает сконструированный сервис:
# FastAPI 0.110+, Python 3.11+
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_async_session
from app.services.orders import OrderService
from app.config import settings
async def get_order_service(
session: AsyncSession = Depends(get_async_session),
) -> OrderService:
# Фабрика: собираем сервис с явными параметрами
return OrderService(
session=session,
max_items_per_order=settings.max_items_per_order,
)
Теперь OrderService — обычный класс с конструктором. В тесте его можно создать без FastAPI, передав мок сессии и любой лимит.
Кеширование зависимостей: use_cache и его подвохи
По умолчанию FastAPI кеширует результат зависимости в пределах одного запроса. Если get_async_session встречается в двух местах графа, вызов произойдёт один раз — оба потребителя получат один и тот же объект сессии. Это поведение контролируется параметром use_cache в Depends().
| Ситуация | use_cache=True (по умолчанию) | use_cache=False |
|---|---|---|
| Два потребителя одной сессии БД | Одна сессия, одна транзакция — ожидаемо | Две независимые сессии — неожиданные коммиты |
| Генератор текущего пользователя | Один запрос в базу за запрос | Повторный запрос — лишняя нагрузка |
| Фабрика с побочным эффектом (счётчик, метрика) | Эффект сработает один раз | Эффект сработает столько раз, сколько зависимость упоминается |
Проблема возникает, когда разработчик не осознаёт кеширование и полагается на то, что зависимость вызывается каждый раз. Или наоборот — ставит use_cache=False, не понимая, что генератор с yield при этом создаст второй ресурс, который тоже нужно будет корректно закрыть.
Тестируемость: dependency_overrides и его границы
FastAPI предлагает app.dependency_overrides — словарь, в который можно положить подмену для любой зависимости. Механизм мощный, но у него есть цена.
Что работает хорошо
# conftest.py
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
from app.db import get_async_session
async def override_session():
# Тестовая сессия с откатом транзакции
async with test_engine.connect() as conn:
transaction = await conn.begin()
session = AsyncSession(bind=conn)
yield session
await transaction.rollback()
@pytest.fixture
async def client():
app.dependency_overrides[get_async_session] = override_session
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()
Подмена инфраструктурной зависимости — классический сценарий. Тест получает реальную сессию к тестовой базе, транзакция откатывается после каждого теста.
Где начинаются проблемы
- Глобальное состояние.
dependency_overrides— мутабельный словарь на объекте приложения. Если тесты запускаются параллельно (pytest-xdist), один тест может перезаписать подмену другого. Решение — либо отдельный экземплярappна воркер, либо фикстуры с жёстким cleanup. - Ключ — сам объект функции. Если вы импортируете
get_async_sessionиз разных мест и где-то сделали реимпорт или обёртку, ключ не совпадёт и подмена не сработает. Отладка этого занимает часы. - Вложенные зависимости не подменяются «каскадом». Если вы подменили
get_current_user, но не подменилиget_async_session, от которой он зависит, — подменаget_current_userвызовется целиком ваша, аget_async_sessionвнутри неё FastAPI вызывать не будет. Это логично, но неочевидно: при частичной подмене графа легко получить тесты, которые проходят случайно.
Сноска: dependency_overrides — словарь вида {callable: callable}, встроенный в экземпляр FastAPI. Перед вызовом любой зависимости фреймворк проверяет, нет ли для неё подмены в этом словаре. Если есть — вызывает подмену вместо оригинала. Механизм работает только при запуске через ASGI-приложение (в тестах через TestClient или AsyncClient), а не при прямом вызове функции.
Граф зависимостей: когда он становится проблемой
FastAPI строит граф зависимостей для каждого эндпоинта при старте. Это дёшево. Но человек, читающий код, видит только сигнатуру обработчика — граф приходится восстанавливать в голове. Два практических правила, которые снижают когнитивную нагрузку:
- Глубина вложенности — не больше трёх. Обработчик → фабрика сервиса → сессия БД. Если появляется четвёртый уровень, скорее всего, что-то можно объединить или вынести из графа.
- Одна зависимость — одна ответственность. Если
get_current_userодновременно логирует вход и обновляетlast_seen, тест на авторизацию начинает зависеть от записи в базу. Разделите: одна зависимость возвращает пользователя, побочный эффект — в middleware или отдельном хуке.
Когда Depends — не лучший выбор
Не всё, что можно оформить как зависимость, нужно оформлять как зависимость. Конфигурация, которая не меняется между запросами, — это просто импортированный объект. Утилита, которая не требует состояния запроса, — обычная функция. Злоупотребление Depends для таких вещей усложняет граф и делает тесты тяжелее без видимой выгоды.
Что ломается первым
Три граничных случая, которые регулярно всплывают в проде и на ревью:
- Генератор-зависимость и необработанное исключение. Если код после
yieldпадает, FastAPI подавляет ошибку и клиент получает 500 без внятного лога. Код очистки послеyieldдолжен быть обёрнут вtry/finally— всегда. - Синхронная зависимость в async-обработчике. FastAPI запустит её в тредпуле. Если внутри — блокирующий вызов длиной в секунду, тредпул забивается, и весь сервис начинает отвечать медленнее. Это не баг FastAPI, а следствие архитектуры: async-обработчики живут в event loop, синхронные зависимости — нет.
- Циклические зависимости. FastAPI не проверяет циклы на старте — вы получите
RecursionErrorпри первом запросе. Обнаружить цикл в графе из двух зависимостей легко, из двадцати — не очень.
Что делать дальше
После того как границы зависимостей выстроены, следующий шаг — наблюдаемость. Добавьте middleware, который логирует время выполнения каждой зависимости: это покажет, где граф реально тратит время. В FastAPI нет встроенного хука для этого, но можно обернуть фабрики в декоратор с замером. Отдельная тема — интеграция с инструментами вроде OpenTelemetry для трассировки зависимостей в распределённых системах, но это уже за рамками текущего разбора.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: FastAPI — Dependencies (официальная документация), FastAPI — Testing Dependencies with Overrides
Частые вопросы
Можно ли использовать классы вместо функций в Depends?
Зависимости в FastAPI — это синглтоны?
Тесты падают с ошибкой, хотя dependency_overrides задан — в чём причина?
Как тестировать эндпоинт без dependency_overrides — напрямую?
Что будет, если зависимость с yield выбросит исключение после yield?
Работает ли Depends с WebSocket-эндпоинтами?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.