首页
/ FastAPI 测试中的依赖覆盖:app.dependency_overrides 原理与实战

FastAPI 测试中的依赖覆盖:app.dependency_overrides 原理与实战

2026-09-07 16:41:30作者:管翌锬

在 FastAPI 应用中,依赖(Depends)常常封装了数据库会话、外部认证服务等开销较大或不可控的逻辑,直接运行完整测试套件时会带来成本与耗时问题。本篇基于仓库中 testing-dependencies 文档 及配套示例代码,系统讲解 app.dependency_overrides 的工作方式:如何在测试中用自定义函数替换任意位置的原始依赖、如何正确重置覆盖,并结合 FastAPI 源码解析覆盖机制的底层实现,使读者既能复制即用,也能理解其内部原理。

什么时候需要覆盖依赖

FastAPI 的文档给出了典型场景:测试期间,你不想执行某个原始依赖(它可能还挂着若干子依赖),而是希望提供一个仅在测试期间生效的替代依赖,由它返回一个固定值,供原来使用依赖返回值的位置消费。

最典型的用例是外部服务,例如:

  • 应用依赖一个外部认证 Provider:向它发送 token,它返回已认证用户;
  • 该 Provider 可能按请求次数计费,且每次调用比使用固定的 mock 用户更慢;
  • 你可能只想针对该 Provider 本身做一次集成测试,而不希望在每次运行测试时都真实调用它。

此时正确的做法是:把"调用该 Provider 的依赖"整体覆盖掉,在测试中使用一个返回 mock 用户的自定义依赖。这样测试既快又省钱,也不受外部服务可用性影响。

核心机制:app.dependency_overrides 属性

FastAPI 应用实例上有一个 app.dependency_overrides 属性,它是一个普通的 dict。覆盖规则非常直接:

  • 键(key):原始依赖函数本身(函数对象);
  • 值(value):用于替代它的覆盖依赖(另一个函数)。

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

从源码看,该属性在 FastAPI.__init__ 中被初始化为空字典,并带有官方文档注释说明其用途就是"用测试版本替换昂贵的依赖",定义位于 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.
        ...
        """
    ),
] = {}

同时注意紧随其后的构造代码:应用把自己的 self 作为 dependency_overrides_provider 传给了内部 router(fastapi/applications.py):

self.router: routing.APIRouter = routing.APIRouter(
    routes=routes,
    redirect_slashes=redirect_slashes,
    dependency_overrides_provider=self,  # 应用即覆盖字典的"提供者"
    ...
)

这意味着覆盖字典由应用统一持有,并沿着路由树向下传播——包括 .include_router() 挂载的子路由,子 router 会从父 router 继承 dependency_overrides_provider(可见 fastapi/routing.pyinclude_router/路由注册链路里对 dependency_overrides_provider 的多处透传)。这正是"依赖定义在何处都可以被覆盖"的结构基础。

完整实战示例

下面给出仓库中 docs_src/dependency_testing/tutorial001_an_py310.py 的完整代码(Annotated 风格写法),它同时包含被测应用与 pytest 测试,可直接复制运行:

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


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 Users!",
        "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},
    }

(该文件还有不使用 Annotated 的兼容写法,见 docs_src/dependency_testing/tutorial001_py310.py,核心逻辑完全一致。)

代码逐段解析

  1. 原始依赖 common_parameters:声明了三个查询参数 q(可选字符串)、skip(默认 0)、limit(默认 100),返回一个 dict。
  2. 两个端点 /items//users/ 都通过 Depends(common_parameters) 注入该依赖——注意它们都使用同一个依赖函数对象,这正是覆盖能"一处设置、全局生效"的原因。
  3. 覆盖依赖 override_dependency:只声明了 q: str | None = None 一个参数,并固定返回 {"q": q, "skip": 5, "limit": 10}
  4. 注册覆盖app.dependency_overrides[common_parameters] = override_dependency——键是原始依赖函数本身,值是替代函数。
  5. 测试断言验证了三个关键行为
    • 不带任何参数请求 /items/ 时,skiplimit 返回的是覆盖依赖硬编码的 510,而不是原始依赖的默认值 0100
    • ?q=foo 时,q 的取值会透传给覆盖依赖(因为覆盖函数也声明了 q 参数);
    • 即使请求中显式传入 ?q=foo&skip=100&limit=200,响应依然是 skip: 5, limit: 10——覆盖依赖没有声明 skip/limit 参数,这些查询参数既不会参与它的解析,也不会影响它的返回值。

最后一条断言尤其值得注意:它说明覆盖函数的参数签名会被重新解析。也就是说,覆盖函数不是"接收原始依赖的返回值",而是 FastAPI 把它当作一个全新的依赖来求解:按覆盖函数自己的签名提取参数、做校验,再执行。

为什么"任意位置"的依赖都能被覆盖

FastAPI 文档特别提示:无论原始依赖出现在 path operation 函数参数path operation 装饰器dependencies=[...],即不使用其返回值时)、还是 .include_router() 挂载的路由中,FastAPI 都能对其设置覆盖。

源码级机制

覆盖的实际发生在依赖求解入口 solve_dependencies,位于 fastapi/dependencies/utils.py。关键逻辑如下(节选 L619-L651):

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_path: str = sub_dependant.path  # type: ignore
        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(
        request=request,
        dependant=use_sub_dependant,
        ...
        dependency_overrides_provider=dependency_overrides_provider,
        ...
    )

从这段代码可以确认三个事实:

  1. 以原始函数对象为键做字典查找dependency_overrides.get(original_call, original_call)。查找用的是函数对象的标识,所以无论该依赖最初是在端点函数、装饰器 dependencies 参数还是 router 上注册的,只要它作为 Depends 的目标函数出现在依赖树中,就能命中同一个覆盖项。
  2. 覆盖函数被 get_dependant 重新解析:命中覆盖后,FastAPI 用覆盖函数重建了 Dependant(参数、校验规则全部按覆盖函数自己的签名生成),这解释了上面示例中"覆盖依赖只认 q、无视 skip/limit"的行为。
  3. 递归传播solve_dependencies 在处理每个子依赖时会把自己递归调用,并把 dependency_overrides_provider 原样传入。因此覆盖不仅作用于顶层依赖,依赖的子依赖同样可以被覆盖——你不需要也不应该去"逐个替换"依赖树内部节点,直接对想替换的那个依赖函数设置覆盖即可,它整条子树都不会再执行。

测试用例佐证

仓库中的 tests/test_dependency_overrides.py 正是围绕"覆盖位置无关性"设计的:它定义了四个路由,分别对应依赖的四种常见出现位置——

路由 依赖出现位置
/main-depends/ path operation 函数参数中的 Depends
/decorator-depends/ path operation 装饰器的 dependencies=[Depends(...)]
/router-depends/ 通过 APIRouter 注册的函数参数 Depends
/router-decorator-depends/ router 路由装饰器的 dependencies=[...]

该文件随后定义覆盖函数并验证这些位置上的依赖都被替换(其中还包含"覆盖依赖自身携带子依赖"的用例,见 overrider_dependency_with_sub,L43-L48)。此外,教程示例的测试位于 tests/test_tutorial/test_testing_dependencies/test_tutorial001.py,可对照验证本文示例的断言行为。

重置覆盖

覆盖设置后不会自动消失,必须显式清理。官方给出的重置方式是直接把字典清空:

app.dependency_overrides = {}

文档同时给出了一条实用建议:如果只想在部分测试期间生效覆盖,就在测试开始时(测试函数内部)设置覆盖,在测试结束时(测试函数末尾)重置。这样可以避免覆盖"泄漏"到其他测试,造成测试之间相互污染、难以排查的问题。

在 pytest 项目中,这一"设置/重置"的配对逻辑通常配合 fixture 的 setup/teardown 阶段管理(如 yield 风格的 fixture),保证每个测试函数结束后覆盖一定被清理;具体组织方式可参考上面 tests/test_dependency_overrides.py 对多个覆盖场景的编排方式。

小结

app.dependency_overrides 是 FastAPI 提供的官方测试注入机制,核心要点可以归纳为:

  • 它是一个 dict键为原始依赖函数对象,值为替代函数
  • 覆盖在依赖求解阶段(solve_dependencies)按函数身份命中,覆盖函数按自己的签名重新解析参数,其整棵子依赖树都不会执行;
  • 覆盖对任何注册位置(端点参数、装饰器 dependencies、router 级注册)均生效,因为覆盖字典由应用持有并经 dependency_overrides_provider 沿路由树传播;
  • 测试结束后务必执行 app.dependency_overrides = {} 重置,必要时在单个测试函数内设置/重置以隔离影响范围。

掌握该机制后,你可以在不改动业务代码的前提下,把外部服务、数据库会话、认证逻辑等高成本依赖整体替换为 mock,使测试套件在离线、快速、可重复的条件下稳定运行。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390