FastAPI 事件测试指南:用 TestClient 触发 lifespan 与 startup/shutdown 事件
在编写 FastAPI 应用测试时,事件钩子(lifespan、startup、shutdown)是否会在测试中被真正执行,直接决定了测试的有效性——数据库连接、缓存初始化、内存数据装载等副作用都发生在这里。本篇指南讲解如何在测试中使用 TestClient 的 with 语句让 lifespan 事件正常运行,以及针对已弃用的 startup/shutdown 事件的处理方式,并结合 FastAPI 仓库源码说明事件参数的注册位置与弃用标记的实现,帮助你写出能完整验证"启动即有数据、关闭即清理"行为的测试。
为什么测试中需要显式触发 lifespan
FastAPI 应用的事件钩子通常用于在应用启动时做初始化(连接数据库、加载配置、填充缓存),在应用停止时做清理。当应用以测试客户端(而非真实 ASGI 服务器)驱动时,事件是否执行取决于测试代码如何构造 TestClient。FastAPI 测试套件中的约定是:只有把 TestClient 放在 with 语句(上下文管理器)中使用时,lifespan 事件才会被执行。这对应真实服务器"启动应用 → 处理请求 → 终止应用"的完整生命周期。
这一点在 FastAPI 的事件参数定义中也有体现:fastapi/applications.py 中 FastAPI.__init__ 的 lifespan 参数文档明确说明,它是一个 Lifespan 上下文管理器处理器,用于替代 startup 和 shutdown 函数列表,将两者合并为单个上下文管理器:
lifespan: Annotated[
Lifespan[AppType] | None,
Doc(
"""
A `Lifespan` context manager handler. This replaces `startup` and
`shutdown` functions with a single context manager.
"""
),
] = None,
用 with 语句在测试中运行 lifespan
当你的测试需要 lifespan 被执行时,把 TestClient(app) 放在 with 语句中即可。下面的完整示例来自 docs_src/app_testing/tutorial004_py310.py,它在一个异步上下文管理器中完成初始化与清理,并在测试里断言事件各阶段的副作用:
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]
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块之前:lifespan 尚未开始,共享状态items仍为空字典;with TestClient(app) as client:块内:进入块时 lifespan 的yield之前部分已经执行完毕,items被填充;此时发起请求能正常拿到数据,且请求完成后数据依然存在(说明清理还没发生);with块结束后:退出块模拟应用被终止,lifespan 的yield之后部分执行,items.clear()生效,断言共享状态恢复为空。
也就是说,with 语句的进入点触发启动逻辑,退出点触发关闭逻辑,测试可以在两个边界处分别断言副作用,从而验证初始化和清理两条路径都按预期工作。该测试函数本身也是 pytest 可直接执行的测试用例,仓库中的 tests/test_tutorial/test_testing/test_tutorial004.py 直接导入并调用它来验证整个流程:
from docs_src.app_testing.tutorial004_py310 import test_read_items
def test_main():
test_read_items()
如果你希望了解 with TestClient(app) 触发 lifespan 的底层机制(其基于 ASGI 的 asgi.lifespan 协议实现),官方 Starlette 文档站的 "Running lifespan in tests" 一节有详细说明(FastAPI 的 TestClient 即直接复用 Starlette 的实现,见下文源码分析)。
已弃用的 startup / shutdown 事件如何测试
对于已弃用的 startup 与 shutdown 事件(通过 @app.on_event("startup") / @app.on_event("shutdown") 装饰器注册),测试方式与上面一致:同样把 TestClient(app) 放入 with 语句中即可触发事件。示例来自 docs_src/app_testing/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"}
需要强调的是:on_event 已经是**弃用(deprecated)**写法。从源码看,fastapi/applications.py 中的 FastAPI.on_event 方法被 @deprecated 装饰器包裹,警告信息明确指出 on_event is deprecated, use lifespan event handlers instead,其实现只是转发给 self.router.on_event(event_type);同样,FastAPI.__init__ 的 on_startup / on_shutdown 参数文档(fastapi/applications.py)也注明应改用 lifespan 处理器。新代码应统一采用 lifespan 写法;上面这段 startup 事件示例主要用于帮助维护既有代码时的测试,以及理解旧事件与新 lifespan 在测试层面行为的一致性。
源码与测试佐证
TestClient 的来源。 fastapi/testclient.py 只有一行核心实现:
from starlette.testclient import TestClient as TestClient # noqa
即 FastAPI 的 TestClient 完全由 Starlette 提供,with TestClient(app) 触发 lifespan 的能力继承自 Starlette 测试客户端的 ASGI lifespan 支持,FastAPI 层没有额外封装。
弃用警告在测试中的体现。 由于 on_event 会发出 DeprecationWarning,仓库测试 tests/test_tutorial/test_testing/test_tutorial003.py 在导入该示例时用 pytest.warns(DeprecationWarning) 显式包裹,验证了弃用标记确实生效:
import pytest
def test_main():
with pytest.warns(DeprecationWarning):
from docs_src.app_testing.tutorial003_py310 import test_read_items
test_read_items()
lifespan 的更多行为验证。 tests/test_router_events.py 中还覆盖了 lifespan 在 APIRouter 嵌套场景下的行为(如 test_app_lifespan_state、test_router_nested_lifespan_state、test_router_sync_generator_lifespan 等),包括 Router 级 lifespan 与 App 级 lifespan 的合并、父级覆盖子级 state 等情况。如果你的应用把部分初始化逻辑放在 APIRouter(lifespan=...) 上,这些测试用例可以作为编写对应测试时的参照。
小结
- 测试中需要 lifespan 事件运行 → 用
with TestClient(app) as client:,进入块触发启动逻辑,退出块触发关闭逻辑; - 旧式
@app.on_event("startup"/"shutdown")已弃用(源码中的@deprecated标记可证),测试写法相同但新代码应迁移到lifespan; - 在
with块的三个位置(进入前、块内、退出后)分别断言共享状态,即可完整覆盖"初始化 → 服务请求 → 清理"整个事件生命周期; - 相关示例与验证代码位于 docs_src/app_testing/tutorial004_py310.py、docs_src/app_testing/tutorial003_py310.py 及 tests/test_tutorial/test_testing/。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00