FastAPI 如何在测试中运行 lifespan 启动与关闭事件?
2026/9/12 20:58:20 网站建设 项目流程

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块时运行lifespanyield之前的启动代码,退出with块时模拟应用终止并运行yield之后的关闭代码。本文基于 Testing Events 文档 与 Lifespan Events 文档 给出完整可运行的测试写法。

前提:用 lifespan 定义应用

lifespan是一个用@asynccontextmanager装饰的、带yield的异步函数,通过FastAPIlifespan参数传入。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参数,startupshutdown事件处理器将不再被调用——是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中已有foobar两个值,请求/items/foo返回 200 且 JSON 为{"name": "Fighters"}
  • 退出with块之后:TestClient模拟应用终止,lifespanyield之后的清理代码执行,items变回空字典。

运行与验证

测试用 pytest 运行,官方文档中的运行命令为:

$ uv run pytest

成功条件是上述断言全部通过:进入with块前后items的内容变化、请求状态码与响应体、以及退出with块后items被清空。

已废弃的 startup / shutdown 事件写法

如果你的应用仍在使用已废弃的@app.on_event("startup")事件(见 events 文档),TestClientwith用法同样适用,测试写法不变,只是应用定义改为事件形式(完整文件见 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并使用AsyncClientTestClient的那套魔法不再工作,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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询