ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FastAPI 依赖覆盖测试指南:用 app.dependency_overrides 以 Mock 依赖替换昂贵的外部服务调用

FastAPI 依赖覆盖测试指南:用 app.dependency_overrides 以 Mock 依赖替换昂贵的外部服务调用 FastAPI 依赖覆盖测试指南用 app.dependency_overrides 以 Mock 依赖替换昂贵的外部服务调用【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi在 FastAPI 的测试体系中直接让测试命中真实的外部依赖认证服务、付费 API、慢速第三方接口既慢又贵。本篇指南讲解app.dependency_overrides这一官方提供的测试机制如何注册依赖覆盖、它为什么能作用于应用中任意位置的依赖声明、以及如何在测试结束后正确重置覆盖并结合仓库源码说明覆盖生效的底层调用链。读完本文你将掌握在不修改业务代码的前提下为任意依赖注入 Mock 实现并用TestClient验证其行为的完整方案。测试中为什么需要覆盖依赖FastAPI 的依赖注入系统允许把公共逻辑参数解析、认证、数据库会话等抽成函数通过Depends注入到路由函数、路由装饰器参数或.include_router()调用中。但在测试场景下有些依赖不应被执行原因通常包括成本例如你接入了一个外部认证提供方——把 Token 发过去换回一个已认证用户。如果该提供方按请求计费那么每一条测试请求都在花钱速度外部网络调用的延迟远大于返回一个预定义好的 Mock 用户隔离性你只想对那个提供方做一次真实的集成验证而不是在每个单元测试里都调用它。典型的处理方式是保留一条真实调用一次的集成测试而在其余测试中用一条覆盖规则把该依赖替换成返回 Mock 数据的函数且仅在测试期间甚至只在某些特定测试期间生效。app.dependency_overrides一个简单的字典为支持上述场景FastAPI应用提供了一个属性app.dependency_overrides它是一个普通的dict字典。使用方式只有两条规则键key原始依赖一个函数值value用于覆盖它的函数另一个函数。之后FastAPI在解析依赖时会调用你的覆盖函数而不是原始依赖。在源码中该属性定义于 FastAPI 应用构造函数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. ... ), ] {}其类型标注清楚地说明了键值语义键是原始依赖 callable值是真正被调用的依赖 callable初始值为空字典{}。构造函数同时把应用自身传入主路由routing.APIRouter(..., dependency_overrides_providerself, ...)——也就是说应用实例就是所有路由的覆盖提供方这正是覆盖能作用于任意位置依赖的原因。提示你可以为在FastAPI应用的任何位置被使用的依赖设置覆盖。原始依赖可能是某条路径操作函数路径操作函数中的参数依赖某个路径操作装饰器参数dependencies[...]即使你不使用其返回值某个.include_router()调用上的依赖等等。无论原始依赖声明在哪一层FastAPI 都可以将其覆盖。仓库测试 tests/test_dependency_overrides.py 中的用例覆盖了路径操作依赖、装饰器级依赖/decorator-depends/和路由级依赖/router-depends/验证了这些位置都会命中覆盖规则。完整示例覆盖一个公共参数依赖以下代码来自官方文档示例 docs_src/dependency_testing/tutorial001_an_py310.py另有非Annotated风格版本 tutorial001_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 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/?qfoo) 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/?qfooskip100limit200) assert response.status_code 200 assert response.json() { message: Hello Items!, params: {q: foo, skip: 5, limit: 10}, }示例要点拆解common_parameters是原始依赖接收查询参数q、skip、limit并原样返回一个字典。它同时被/items/和/users/两个路由使用override_dependency是覆盖函数它只接收q参数skip和limit则被硬编码为5和10。注意覆盖函数可以有自己不同的签名——测试中?skip100limit200这类参数会被覆盖函数直接忽略最终响应里的值永远是{skip: 5, limit: 10}app.dependency_overrides[common_parameters] override_dependency一行完成注册以原始函数对象为键、覆盖函数为值三个测试分别验证了无参请求、仅传q请求、传齐q/skip/limit三种情况下覆盖均生效且skip/limit固定为覆盖函数的返回值。底层实现覆盖是在依赖求解阶段动态查表的覆盖为什么能生效答案在依赖求解函数 solve_dependencies 中。每当解析一个子依赖时它会执行类似如下逻辑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( pathuse_path, callcall, namesub_dependant.name, parent_oauth_scopes_get_oauth_scopes(dependantsub_dependant), scopesub_dependant.scope, )从源码结构看可以得出三个关键结论查表时机是每次请求解析依赖时而非应用启动时。因此你可以在测试运行中途随时修改app.dependency_overrides下一个请求立即使用新规则这也是按单个测试设置/重置覆盖可行的底层原因查不到则原样执行.get(original_call, original_call)表示若字典中没有该键就回落到原始依赖行为与未开启覆盖完全一致覆盖函数会被重新做一次依赖解析命中覆盖后get_dependant(callcall, ...)用覆盖函数重建了一个Dependant。这意味着覆盖函数自身也可以声明子依赖比如从请求读取k: str参数其参数校验规则按覆盖函数自己的签名生效。仓库测试 tests/test_dependency_overrides.py 中test_override_with_sub_*系列用例验证了这一点带子依赖的覆盖函数在缺少必需参数时会返回 422 校验错误。此外由于solve_dependencies是递归的且递归时透传了同一个dependency_overrides_provider覆盖不仅作用于直接依赖也会递归地作用于被依赖的依赖树中的任意一层。重置覆盖恢复原始依赖测试结束后或某一批测试结束后把app.dependency_overrides设为空字典即可移除全部覆盖app.dependency_overrides {}提示如果你只想在某些测试期间覆盖某个依赖可以在测试开始处测试函数内部设置覆盖在测试结束处测试函数末尾重置它。仓库对教程示例的验证测试 tests/test_tutorial/test_testing_dependencies/test_tutorial001.py 正是这一思路的体现前若干用例断言/items/与/users/的响应都命中了override_dependencyskip: 5, limit: 10而最后一个test_normal_app用例将覆盖清空后再次请求断言?qfooskip100limit200被原样返回——证明重置后应用恢复使用原始依赖。实践建议小结键必须是函数对象本身app.dependency_overrides[common_parameters] ...中写入的是可调用的函数引用而不是字符串名称或Depends()实例覆盖函数签名自由它只接收它自己声明的参数原始依赖的参数定义默认值、校验对覆盖函数不再生效覆盖范围是全局的只要该函数在任何地方被Depends引用路由参数、路由装饰器、include_router都会被替换覆盖是进程内状态它只影响共享同一个app实例的测试。TestClient(app)与生产服务器使用同一个字典因此务必在测试中及时重置避免覆盖泄漏到后续用例与dependency_overrides_provider的关系源码中路由持有的是提供方引用fastapi/applications.py 中APIRouter以dependency_overrides_providerself构建所以读取的始终是app.dependency_overrides当前的值——覆盖在每次请求时实时生效这也是测试中逐用例切换覆盖的可靠基础。至此app.dependency_overrides的完整使用链路为定义原始依赖 → 在测试中注册覆盖 →TestClient发请求验证 Mock 行为 → 重置字典恢复原状。配合仓库中的源码与测试fastapi/dependencies/utils.py、tests/test_dependency_overrides.py你可以进一步验证覆盖在装饰器级、路由级以及带子依赖等边界场景下的行为。【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表