首页
/ FastAPI 事件测试指南:用 TestClient 触发 lifespan 与 startup/shutdown 事件

FastAPI 事件测试指南:用 TestClient 触发 lifespan 与 startup/shutdown 事件

2026-09-07 16:44:24作者:宣海椒Queenly

在编写 FastAPI 应用测试时,事件钩子(lifespanstartupshutdown)是否会在测试中被真正执行,直接决定了测试的有效性——数据库连接、缓存初始化、内存数据装载等副作用都发生在这里。本篇指南讲解如何在测试中使用 TestClientwith 语句让 lifespan 事件正常运行,以及针对已弃用的 startup/shutdown 事件的处理方式,并结合 FastAPI 仓库源码说明事件参数的注册位置与弃用标记的实现,帮助你写出能完整验证"启动即有数据、关闭即清理"行为的测试。

为什么测试中需要显式触发 lifespan

FastAPI 应用的事件钩子通常用于在应用启动时做初始化(连接数据库、加载配置、填充缓存),在应用停止时做清理。当应用以测试客户端(而非真实 ASGI 服务器)驱动时,事件是否执行取决于测试代码如何构造 TestClient。FastAPI 测试套件中的约定是:只有把 TestClient 放在 with 语句(上下文管理器)中使用时,lifespan 事件才会被执行。这对应真实服务器"启动应用 → 处理请求 → 终止应用"的完整生命周期。

这一点在 FastAPI 的事件参数定义中也有体现:fastapi/applications.pyFastAPI.__init__lifespan 参数文档明确说明,它是一个 Lifespan 上下文管理器处理器,用于替代 startupshutdown 函数列表,将两者合并为单个上下文管理器:

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 的一个状态,是可复用的测试写法:

  1. with 块之前:lifespan 尚未开始,共享状态 items 仍为空字典;
  2. with TestClient(app) as client: 块内:进入块时 lifespan 的 yield 之前部分已经执行完毕,items 被填充;此时发起请求能正常拿到数据,且请求完成后数据依然存在(说明清理还没发生);
  3. 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 事件如何测试

对于已弃用的 startupshutdown 事件(通过 @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_statetest_router_nested_lifespan_statetest_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.pydocs_src/app_testing/tutorial003_py310.pytests/test_tutorial/test_testing/
登录后查看全文
热门项目推荐
相关项目推荐