FastAPI:如何在测试中使用 `app.dependency_overrides` 覆盖依赖(Testing Dependencies with Overrides)
2026/9/10 20:27:38 网站建设 项目流程

FastAPI:如何在测试中使用app.dependency_overrides覆盖依赖(Testing Dependencies with Overrides)

【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi

本篇技术指南以 FastAPI 官方文档中《Testando Dependências com Sobreposições》(docs/pt/docs/advanced/testing-dependencies.md)为核心,深入讲解测试阶段通过app.dependency_overrides将真实依赖替换为 Mock 依赖的完整方案。你将掌握:何时需要覆盖依赖、如何用字典语法注册与清除覆盖、覆盖在路由函数/装饰器/include_router等任意场景下如何生效,以及该机制在 FastAPI 源码中的底层实现与仓库测试的验证证据。

为什么要在测试中覆盖依赖

在编写单元测试时,有些场景下我们不希望原始依赖函数真正执行——包括它所携带的任何子依赖(sub-dependencies)也不应执行。

我们的目标是:在测试期间(可能只在某些特定测试中)提供一个不同的依赖实现,它返回的值可以在原本使用原始依赖返回值的位置继续使用。

典型诉求包括:

  • 原始依赖调用外部服务,测试时希望避免真实网络请求;
  • 外部服务按请求计费,不希望每个测试都产生费用;
  • 真实依赖响应较慢(如远程认证服务),固定 Mock 数据能让测试更快、更稳定;
  • 希望针对异常路径、边界数据做确定性断言。

覆盖依赖的核心价值在于:测试只验证应用自身逻辑,而不依赖外部系统的可用性与行为

典型用例:外部认证服务

文档给出的经典场景是外部认证提供方(external authentication provider):

  1. 应用向认证服务发送token
  2. 认证服务返回一个已认证的用户对象;
  3. 该服务可能按请求计费,且调用耗时明显高于本地固定的 Mock 用户;
  4. 我们通常只需要完整地测试一次真实的外部提供方,而不希望每次跑测试都调用它。

解决方案就是:覆盖(override)那个调用提供方的依赖,替换为一个只用于测试的、返回 Mock 用户的自定义依赖

app.dependency_overrides属性:一个简单的字典

FastAPI 应用实例上有一个专门的属性app.dependency_overrides,它就是一个普通的dict(Python 字典)。

覆盖依赖的规则非常简单:

  • 键(key):原始依赖(一个函数对象);
  • 值(value):你的覆盖依赖(另一个函数对象)。

之后,FastAPI 会在解析依赖时调用覆盖函数,而不是原始依赖函数

该属性在源码中的定义位于 fastapi/applications.py:

self.dependency_overrides: Annotated[ dict[Callable[..., Any], Callable[..., Any]], Doc( """ A dictionary with overrides for the dependencies. Each key is the original dependency callable, and the value is the actual dependency that should be called. This is for testing, to replace expensive dependencies with testing versions. ... """ ), ] = {}

可以看到:类型标注为dict[Callable, Callable],即“原始依赖可调用对象 → 覆盖依赖可调用对象”的映射,官方注释明确说明其用途是测试时用测试版本替换昂贵的依赖

完整示例

文档引用的示例代码位于 docs_src/dependency_testing/tutorial001_an_py310.py,核心部分如下:

from typing import Annotated from fastapi import Depends, FastAPI from fastapi.testclient import TestClient app = FastAPI() async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): return {"q": q, "skip": skip, "limit": limit} @app.get("/items/") async def read_items(commons: Annotated[dict, Depends(common_parameters)]): return {"message": "Hello Items!", "params": commons} @app.get("/users/") async def read_users(commons: Annotated[dict, Depends(common_parameters)]): return {"message": "Hello Users!", "params": commons} client = TestClient(app) async def override_dependency(q: str | None = None): return {"q": q, "skip": 5, "limit": 10} app.dependency_overrides[common_parameters] = override_dependency

关键步骤拆解:

  1. 定义原始依赖common_parameters,它从查询参数读取qskiplimit并组装成字典;
  2. 两个路径操作函数read_items/read_users都通过Depends(common_parameters)使用该依赖;
  3. 定义覆盖依赖override_dependency强制返回固定的skip=5limit=10
  4. 通过app.dependency_overrides[common_parameters] = override_dependency完成注册——注意键必须是与Depends()完全相同的函数对象

覆盖后的测试断言

注册覆盖后,无论客户端传入什么参数,应用都会使用覆盖依赖的返回值:

def test_override_in_items(): response = client.get("/items/") assert response.status_code == 200 assert response.json() == { "message": "Hello Items!", "params": {"q": None, "skip": 5, "limit": 10}, } def test_override_in_items_with_q(): response = client.get("/items/?q=foo") assert response.status_code == 200 assert response.json() == { "message": "Hello Items!", "params": {"q": "foo", "skip": 5, "limit": 10}, } def test_override_in_items_with_params(): response = client.get("/items/?q=foo&skip=100&limit=200") assert response.status_code == 200 assert response.json() == { "message": "Hello Items!", "params": {"q": "foo", "skip": 5, "limit": 10}, }

注意第三组断言:即使 URL 传入了skip=100&limit=200,响应仍是skip=5, limit=10——这直接证明原始依赖完全没有执行,覆盖依赖接管了参数解析与返回值构造。这些测试用例同时也被仓库的 tests/test_tutorial/test_testing_dependencies/test_tutorial001.py 自动验证,该测试文件通过importlib动态导入docs_src.dependency_testing.tutorial001_py310tutorial001_an_py310两个变体并逐一运行其中的测试函数。

覆盖对“应用任意位置”的依赖都生效

文档特别强调:你可以为FastAPI 应用中任何位置使用的依赖设置覆盖,原始依赖无论是:

  • 路径操作函数参数中使用(如commons: Annotated[dict, Depends(common_parameters)]);
  • 路径操作装饰器中使用(通过dependencies=[Depends(...)],此时不使用返回值);
  • 通过.include_router()挂载的子路由中使用;

FastAPI 都能正确地覆盖它。

仓库测试 tests/test_dependency_overrides.py 用四个端点完整验证了这一承诺:

@app.get("/main-depends/") async def main_depends(commons: dict = Depends(common_parameters)): return {"in": "main-depends", "params": commons} @app.get("/decorator-depends/", dependencies=[Depends(common_parameters)]) async def decorator_depends(): return {"in": "decorator-depends"} @router.get("/router-depends/") async def router_depends(commons: dict = Depends(common_parameters)): return {"in": "router-depends", "params": commons} @router.get("/router-decorator-depends/", dependencies=[Depends(common_parameters)]) async def router_decorator_depends(): return {"in": "router-decorator-depends"} app.include_router(router)

覆盖common_parameters后,四个端点(主应用参数注入、主应用装饰器、子路由参数注入、子路由装饰器)全部返回覆盖后的固定值(skip=5, limit=10),例如:

def test_override_simple(url, status_code, expected): app.dependency_overrides[common_parameters] = overrider_dependency_simple response = client.get(url) assert response.status_code == status_code assert response.json() == expected app.dependency_overrides = {}

该测试通过参数化(parametrize)遍历/main-depends//decorator-depends//router-depends//router-decorator-depends/四条路径,并在每个用例结束后立即清空覆盖。

覆盖依赖也可以有子依赖

覆盖函数本身同样可以声明自己的子依赖。仓库测试中的overrider_dependency_with_sub演示了这一点:

async def overrider_sub_dependency(k: str): return {"k": k} async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_dependency)): return msg

当用overrider_dependency_with_sub覆盖common_parameters后:

  • 请求/main-depends/不带k参数时,返回422校验错误,错误定位在["query", "k"]——说明 FastAPI 解析了覆盖依赖的子依赖并执行校验;
  • 请求/main-depends/?k=bar时,返回200params{"k": "bar"}——说明覆盖链完整生效。

这印证了文档开头那句“你也不希望原始依赖携带的任何子依赖执行”:覆盖是整棵依赖树的替换,而非单点替换。

清除覆盖:恢复原始依赖

当你需要移除所有覆盖、让应用恢复原状时,只需把app.dependency_overrides重新赋值为空字典:

app.dependency_overrides = {}

这是仓库测试中反复使用的清理模式(见 tests/test_dependency_overrides.py 中每个测试函数末尾的app.dependency_overrides = {})。

只对部分测试生效:在测试函数内部设置与清理

如果你只想在个别测试中覆盖依赖,建议在测试函数开头设置覆盖,并在测试函数结尾重置:

def test_apenas_este_teste(): app.dependency_overrides[common_parameters] = override_dependency # ... 执行断言 ... app.dependency_overrides = {}

这样既不影响其他测试,也能保证用例间相互隔离、避免覆盖泄漏到后续测试。

底层实现:覆盖是如何生效的

覆盖机制的核心实现在 fastapi/dependencies/utils.py 的solve_dependencies()函数中。在解析每个子依赖时,FastAPI 会先检查覆盖提供方:

for sub_dependant in dependant.dependencies: sub_dependant.call = cast(Callable[..., Any], sub_dependant.call) call = sub_dependant.call use_sub_dependant = sub_dependant if ( dependency_overrides_provider and dependency_overrides_provider.dependency_overrides ): original_call = sub_dependant.call call = getattr( dependency_overrides_provider, "dependency_overrides", {} ).get(original_call, original_call) use_sub_dependant = get_dependant( path=use_path, call=call, name=sub_dependant.name, parent_oauth_scopes=_get_oauth_scopes(dependant=sub_dependant), scope=sub_dependant.scope, ) solved_result = await solve_dependencies(...)

实现要点:

  1. 原始依赖函数对象sub_dependant.call)为键,在dependency_overrides字典中查找;
  2. 若命中覆盖,则用get_dependant()基于覆盖函数重新构建依赖分析对象use_sub_dependant,并递归解析其参数与子依赖;
  3. 若未命中,get(original_call, original_call)返回原始函数,行为与平时完全一致。

这个“按函数对象精确匹配”的设计,解释了为什么注册时必须使用与Depends()同一个函数对象作为键。

覆盖提供方的注入链路也很清晰:FastAPI应用在创建自身路由器时,将自己作为dependency_overrides_provider传入(fastapi/applications.py),路由对象再层层传递给solve_dependencies()(见 fastapi/routing.py 中dependency_overrides_provider的传递),最终由 fastapi/dependencies/utils.py 消费。

小结

要点说明
注册覆盖app.dependency_overrides[original_func] = override_func
覆盖范围路径操作函数参数、装饰器dependencies=include_router()子路由中的依赖均生效
覆盖链覆盖函数自身的子依赖也会被完整解析与校验
清除全部覆盖app.dependency_overrides = {}
局部生效在测试函数开头设置、结尾重置
底层原理solve_dependencies()按函数对象查字典,命中则基于覆盖函数重建依赖分析(fastapi/dependencies/utils.py)

通过app.dependency_overrides,你可以在不修改任何业务代码的前提下,为测试注入确定性的 Mock 依赖,让测试套件更快、更稳、更省钱,同时保持对应用全部依赖注入点的完全控制。

【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询