FastAPI 如何在测试中运行 lifespan 启动与关闭事件?
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
如果你的 FastAPI 应用通过lifespan参数在启动时加载共享资源(比如数据库连接池、机器学习模型),那么在测试里直接调用TestClient(app)发请求时,这些启动逻辑并不会执行——因为lifespan没有被触发。FastAPI 官方文档给出的解决方法是:把TestClient放进with语句中使用,进入with块时运行lifespan中yield之前的启动代码,退出with块时模拟应用终止并运行yield之后的关闭代码。本文基于 Testing Events 文档 与 Lifespan Events 文档 给出完整可运行的测试写法。
前提:用 lifespan 定义应用
lifespan是一个用@asynccontextmanager装饰的、带yield的异步函数,通过FastAPI的lifespan参数传入。yield之前的代码在应用开始接收请求之前执行一次,yield之后的代码在应用停止处理请求时执行一次。官方示例(完整源码):
from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.testclient import TestClient items = {} @asynccontextmanager async def lifespan(app: FastAPI): items["foo"] = {"name": "Fighters"} items["bar"] = {"name": "Tenders"} yield # clean up items items.clear() app = FastAPI(lifespan=lifespan) @app.get("/items/{item_id}") async def read_items(item_id: str): return items[item_id]需要说明的限制:如果你传入了lifespan参数,startup和shutdown事件处理器将不再被调用——是lifespan和事件二选一,不能同时生效。
核心写法:把 TestClient 放进 with 语句
官方文档给出的关键做法是一行:用with TestClient(app) as client:替代直接调用TestClient(app)。上面的应用对应的测试(完整文件见 tutorial004_py310.py):
def test_read_items(): # Before the lifespan starts, "items" is still empty assert items == {} with TestClient(app) as client: # Inside the "with TestClient" block, the lifespan starts and items added assert items == {"foo": {"name": "Fighters"}, "bar": {"name": "Tenders"}} response = client.get("/items/foo") assert response.status_code == 200 assert response.json() == {"name": "Fighters"} # After the requests is done, the items are still there assert items == {"foo": {"name": "Fighters"}, "bar": {"name": "Tenders"}} # The end of the "with TestClient" block simulates terminating the app, so # the lifespan ends and items are cleaned up assert items == {}断言的位置体现了lifespan在测试中的执行时机,这也是判断写法是否正确的依据:
with块之前:items == {},说明启动代码尚未运行;with块内部:lifespan已启动,items中已有foo、bar两个值,请求/items/foo返回 200 且 JSON 为{"name": "Fighters"};- 退出
with块之后:TestClient模拟应用终止,lifespan中yield之后的清理代码执行,items变回空字典。
运行与验证
测试用 pytest 运行,官方文档中的运行命令为:
$ uv run pytest成功条件是上述断言全部通过:进入with块前后items的内容变化、请求状态码与响应体、以及退出with块后items被清空。
已废弃的 startup / shutdown 事件写法
如果你的应用仍在使用已废弃的@app.on_event("startup")事件(见 events 文档),TestClient的with用法同样适用,测试写法不变,只是应用定义改为事件形式(完整文件见 tutorial003_py310.py):
from fastapi import FastAPI from fastapi.testclient import TestClient app = FastAPI() items = {} @app.on_event("startup") async def startup_event(): items["foo"] = {"name": "Fighters"} items["bar"] = {"name": "Tenders"} @app.get("/items/{item_id}") async def read_items(item_id: str): return items[item_id] def test_read_items(): with TestClient(app) as client: response = client.get("/items/foo") assert response.status_code == 200 assert response.json() == {"name": "Fighters"}文档同时建议:对于启动和关闭相互关联的逻辑(获取资源后释放资源),应改用lifespan而不是两个独立的事件函数。
边界与限制
- 异步测试中
AsyncClient不触发 lifespan。Async Tests 文档 明确警告:如果测试函数是async def并使用AsyncClient,TestClient的那套魔法不再工作,AsyncClient不会触发 lifespan 事件;文档建议改用 florimondmanca/asgi-lifespan 提供的LifespanManager来确保事件被触发。如果你的测试是普通def函数并使用TestClient,则不受此限制。 - 子应用不执行 lifespan 事件。events 文档 指出,lifespan 事件只会在主应用上执行,不会为通过 Mount 挂载的子应用执行。
- 更多底层细节(ASGI Lifespan Protocol)可参考 Testing Events 文档 中指向的 Starlette 官方文档。
完整的 lifespan 定义方式(含模型加载用例)见 docs/en/docs/advanced/events.md,可运行的测试源码在 docs_src/app_testing/tutorial004_py310.py。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考