ASGI-тест отправил запрос до startup: запускаем lifespan отдельно
Асинхронный тест обращается к приложению через HTTPX ASGITransport. Сокеты не нужны, запрос проходит прямо в ASGI-приложение. Но обработчик не видит ресурс, который обычно создаётся при startup. В настоящем сервере он есть, а в тесте отсутствует. Возникает соблазн создать ресурс вручную и тем самым перестать проверять настоящий запуск.
ASGITransport отвечает за HTTP-вызовы, а запуск и завершение lifespan не входят в его обязанности. Это явно отмечено в документации HTTPX. Для теста можно использовать LifespanManager из asgi-lifespan: сначала войти в контекст жизненного цикла, затем открыть AsyncClient с транспортом. Выход из внешнего контекста проверит и shutdown.
HTTP отдельно от lifespan
Ниже минимальное ASGI-приложение хранит признак готовности. Это позволяет увидеть разницу без базы данных и отдельного фреймворка. HTTP-ответ зависит от того, прошёл ли startup. Смысл теста — запрос внутри корректного времени жизни приложения, а не ручная установка нужного значения перед запросом.
import asyncio
import httpx
from asgi_lifespan import LifespanManager
ready = False
async def app(scope, receive, send):
global ready
if scope["type"] == "lifespan":
while True:
message = await receive()
if message["type"] == "lifespan.startup":
ready = True
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown":
ready = False
await send({"type": "lifespan.shutdown.complete"})
return
elif scope["type"] == "http":
body = b"ready" if ready else b"not ready"
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": body})
async def main():
async with LifespanManager(app) as manager:
transport = httpx.ASGITransport(app=manager.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/")
assert response.text == "ready"
print(response.text)
assert ready is False
asyncio.run(main())
Порядок контекстов и изоляция
Установите зависимости командой python -m pip install httpx asgi-lifespan. На Python 3.12 файл печатает ready. Пример проверен с HTTPX 0.28.1 и asgi-lifespan 2.1.0. Если убрать LifespanManager и оставить только транспорт, ответ будет not ready. Если ошибка возникнет в подготовке, нужно исправлять startup, а не заменять его тестовой переменной.
Порядок контекстов имеет значение. Клиент должен завершить работу до shutdown, пока ресурсы приложения ещё доступны. Если приложение передаёт состояние через lifespan scope, используйте manager.app, как в примере: менеджер предоставляет обёртку с поддержкой этого состояния. Это особенно важно при реальных соединениях и ресурсах, созданных на старте.
Для набора тестов определите область жизни fixture осознанно. Один startup на тест хорошо изолирует изменяемое состояние, но может быть дорогим. Общий startup на несколько тестов требует очистки предметных данных между сценариями. Не оставляйте решение случайному scope: иначе независимые тесты начнут зависеть от оставшихся в памяти объектов.
Транспорт без сети проверяет приложение на границе ASGI. Настройки настоящего HTTP-сервера, прокси и сокетов остаются отдельным уровнем проверки. Здесь полезно проверить успешный запрос после startup и освобождение ресурса после выхода. Такой тест ловит ошибки времени жизни, которые не видны при прямом вызове функции обработчика, и сохраняет быстрый цикл разработки.
Тест времени жизни полезен, когда он защищает цельный API. В бесплатном курсе «FastAPI для начинающих: API с базой данных и тестами» вы последовательно собираете сервис с проверкой входных данных, хранением, регистрацией, JWT и контролем доступа. В программе есть pytest, fixtures и dependency_overrides: можно связать отдельную проверку с остальными сценариями приложения.
Посмотреть программу и практику курса.
Документация: официальный источник по теме.
Другие задачи по теме: База, HTTP-клиенты и тесты API.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
