第27章:HTTP Mock测试——responses与requests-mock
1. 项目背景业务场景某支付平台的订单服务需要调用银行扣款 API。团队为这个服务编写了单元测试——但测试跑一次要 15 秒真实网络请求到银行测试环境而且非工作时间银行测试环境会维护22:00-06:00 不可用。更糟糕的是某次测试中误用了生产环境的 API Key测试脚本写了死数据导致银行测试环境真的扣了款——测试数据变成了真实交易引发了一场紧急回滚。团队决定引入 HTTP Mock——让测试在零网络依赖、毫秒级完成的情况下运行。但两个主流 Mock 库的出现让团队产生了选择困难responses拦截requests模块级别的调用和requests-mock作为 Adapter 拦截 Session 级别的调用。痛点问题一测试依赖外部服务——慢、不稳定、不可控。依赖真实 API 的单元测试有三大问题1慢网络延迟 100ms/次2不稳定外部服务随时可能宕机3不可控无法模拟 5xx 错误、超时、限流等异常场景。如果测试失败是因为银行测试环境挂了而不是代码 bug开发和排查成本都很高。问题二Mock 库的选择困惑。responses和requests-mock都能拦截 HTTP 请求并返回预设响应但它们的实现机制不同。responses通过 monkey-patchurllib3的适配器来拦截所有请求包括通过requests.get()顶层函数发出的。requests-mock通过在 Session 上 mount 一个 Mock Adapter 来拦截请求——对顶层requests.get()不生效。如果选错了库Mock 可能不生效测试中偷偷发出了真实请求或者误拦截了不该拦截的请求。问题三Mock 不够真实导致测试失真。简单的Mock返回 {status: ok}可能让测试通过但真实 API 的响应可能包含很多复杂字段——嵌套 JSON、特定 HTTP 头、延迟等。如果 Mock 不够真实测试通过的代码在生产环境可能崩溃比如没处理某个意外为 null 的字段。responses vs requests-mock 对比responses: 拦截层面: urllib3 adapter 层全局 适用场景: 拦截所有 HTTP 请求包括顶层 requests.get() 注册方式: responses.activate 装饰器或上下文管理器 Mock 粒度: URL 精确匹配或正则匹配 requests-mock: 拦截层面: Session adapter 层 适用场景: 需要精确控制特定 Session 的 Mock 注册方式: with requests_mock.Mocker() as m: Mock 粒度: URL 精确匹配、正则匹配、自定义匹配函数2. 项目设计小胖在晚上 11 点加班对着测试报错发呆“大师我的单元测试又挂了——但不是代码问题是银行测试环境在维护而且我发现测试跑了 50 个用例每个都要真实调银行 API总共要跑 3 分钟——CI 流水线经常超时”大师“单元测试的第一原则——不依赖外部服务。你的测试应该在任何时间、任何网络环境下都能跑而且应该在毫秒级完成。你需要 HTTP Mock。”小胖眼睛一亮“Mock就是假装调了 API实际上返回一个预设的响应那怎么用”大师“有两个主流选择——responses和requests-mock。我先给你看最简单的responses”importresponsesimportrequestsresponses.activatedeftest_my_api():# 注册 Mock当请求这个 URL 时返回预设的响应responses.get(https://api.bank.com/charge,json{status:success,txn_id:TXN123},status200,)# 业务代码完全不知道自己在调 Mockresprequests.post(https://api.bank.com/charge,json{amount:100})assertresp.json()[status]success# 通过小白追问“那responses和requests-mock有什么区别我该选哪个”大师“关键区别responses是全局拦截——你激活它之后所有requests发出的请求不管是requests.get()还是session.get()都会被拦截。requests-mock是在 Session 级别挂载一个 Mock Adapter只有通过那个特定 Session 发出的请求才会被拦截。”“选型建议如果你的代码用的是requests.get()顶层函数没有 Session用responses。如果你有明确的 Session 对象用requests-mock更精准——不会误拦截其他测试的请求。”小胖“那如何 Mock 异常场景比如超时、500、连接失败——这些在真实环境很难触发。”大师“这正是 Mock 的最大价值——你可以 Mock 任何场景包括那些在真实环境几乎不会遇到但必须处理的异常”# Mock 超时responses.get(url,bodyrequests.exceptions.Timeout())# Mock 500 错误responses.get(url,status500,json{error:Internal Server Error})# Mock 网络错误responses.get(url,bodyrequests.exceptions.ConnectionError())生活比喻技术映射飞行员模拟仓不用真飞机训练HTTP Mock 零网络测试预设发动机故障场景训练Mock 5xx 异常场景模拟信号发生器预设 Mock 响应只测刹车系统不测引擎requests-mock 精准拦截特定 Session全车检测responses 全局拦截3. 项目实战环境准备pipinstallrequests responses requests-mock pytest分步实现步骤一responses——全局 Mock 拦截目标使用 responses 拦截所有 HTTP 请求返回预设响应。importresponsesimportrequests# 基础用法Mock GET 请求 responses.activatedeftest_basic_mock():基础 Mock——拦截请求并返回预设 JSONresponses.get(https://api.example.com/users,json{users:[{id:1,name:小胖}]},status200,)resprequests.get(https://api.example.com/users)assertresp.status_code200assertresp.json()[users][0][name]小胖print(基础 Mock 通过 ✓)# 高级用法Mock 不同 HTTP 方法和状态码 responses.activatedeftest_multiple_mocks():Mock 多种 HTTP 方法和异常状态码# Mock GET——成功responses.get(https://api.example.com/items/1,json{id:1,name:item1},status200,)# Mock POST——创建成功responses.post(https://api.example.com/items,json{id:2,name:new_item},status201,)# Mock GET——404responses.get(https://api.example.com/items/999,json{error:Not Found},status404,)# Mock GET——500responses.get(https://api.example.com/items/500,json{error:Internal Error},status500,)# 执行测试assertrequests.get(https://api.example.com/items/1).status_code200assertrequests.post(https://api.example.com/items).status_code201assertrequests.get(https://api.example.com/items/999).status_code404assertrequests.get(https://api.example.com/items/500).status_code500print(多方法/多状态码 Mock 通过 ✓)# Mock 异常场景 responses.activatedeftest_mock_exceptions():Mock 网络异常超时、连接失败# Mock 超时responses.get(https://api.example.com/slow,bodyrequests.exceptions.Timeout(),)# Mock 连接错误responses.get(https://api.example.com/offline,bodyrequests.exceptions.ConnectionError(),)# 验证超时try:requests.get(https://api.example.com/slow,timeout1)assertFalse,应该抛出超时异常exceptrequests.exceptions.Timeout:print(超时 Mock 通过 ✓)# 验证连接错误try:requests.get(https://api.example.com/offline)assertFalse,应该抛出连接异常exceptrequests.exceptions.ConnectionError:print(连接错误 Mock 通过 ✓)# 运行测试 test_basic_mock()test_multiple_mocks()test_mock_exceptions()步骤二requests-mock——Session 级别精准拦截目标使用 requests-mock 在 Session 级别进行精准 Mock。importrequests_mockimportrequestsdeftest_with_requests_mock():requests-mock 拦截特定 Session 的请求# 使用上下文管理器只拦截 inside 的请求withrequests_mock.Mocker()asm:# 注册 Mock 路由m.get(https://api.example.com/users,json{users:[{id:1}]},status_code200,)m.post(https://api.example.com/users,json{id:2,created:True},status_code201,)# 正则匹配m.get(requests_mock.ANY,json{fallback:True},)# 业务代码通过 Session 发请求sessionrequests.Session()respsession.get(https://api.example.com/users)assertresp.json()[users][0][id]1respsession.post(https://api.example.com/users)assertresp.json()[created]isTrue# 不匹配任何注册路由的请求返回通配 Mockrespsession.get(https://api.example.com/anything)assertresp.json()[fallback]isTrueprint(requests-mock 通过 ✓)deftest_requests_mock_matchers():requests-mock 的高级匹配功能withrequests_mock.Mocker()asm:# JSON 请求体验证m.post(https://api.example.com/orders,additional_matcherlambdareq:req.json().get(amount)0,json{order_id:ORD-001},status_code201,)sessionrequests.Session()# 应该匹配amount 0respsession.post(https://api.example.com/orders,json{amount:100,item:book},)assertresp.status_code201# 不应该匹配未注册对应的 Mocktry:respsession.post(https://api.example.com/orders,json{amount:-1,item:return},)# 可能返回通配 Mock 或 NoMockAddressexceptrequests_mock.exceptions.NoMockAddress:print(预期负金额请求未被 Mock ✓)test_with_requests_mock()test_requests_mock_matchers()步骤三pytest fixture——可复用的 Mock 配置目标封装 pytest fixture在多个测试间复用 Mock 配置。importpytestimportrequestsimportresponses# responses fixture pytest.fixturedefmock_api():统一的 API Mock fixturewithresponses.RequestsMock()asrsps:# 注册通用 Mockrsps.get(https://api.example.com/health,json{status:ok},status200,)rsps.get(https://api.example.com/users/1,json{id:1,name:小胖,email:xiaopangtest.com},status200,)rsps.post(https://api.example.com/users,json{id:2,name:new_user},status201,)yieldrsps# 使用 fixturedeftest_get_user(mock_api):使用 fixture 中的 Mockresprequests.get(https://api.example.com/users/1)assertresp.status_code200assertresp.json()[name]小胖deftest_create_user(mock_api):resprequests.post(https://api.example.com/users)assertresp.status_code201# requests-mock fixture pytest.fixturedefmock_session():带 Mock 的 Session fixtureimportrequests_mockwithrequests_mock.Mocker()asm:m.get(https://api.example.com/data,json{result:mocked})m.post(https://api.example.com/action,json{status:done},status_code200)yieldmdeftest_with_mock_session(mock_session):sessionrequests.Session()respsession.get(https://api.example.com/data)assertresp.json()[result]mocked可能遇到的坑及解决方法坑1responses 未激活时忘记 responses.activate# 如果不用装饰器Mock 注册了但不会生效# 请求会发到真实的 URL# 解决始终用 responses.activate 或 with responses.RequestsMock()坑2requests-mock 对顶层 requests.get() 不生效# requests_mock 只拦截通过注册了 Mock Adapter 的 Session 发出的请求# requests.get() 创建的是临时 Session不会被拦截# 解决用 responses 替代或确保代码使用 Session坑3Mock 注册顺序影响匹配# responses 按注册顺序匹配先匹配到的生效# 将更具体的 URL 注册在前面通用匹配在最后测试验证importpytestimportrequestsimportresponsesclassTestMockStrategy:验证 Mock 测试策略responses.activatedeftest_mock_prevents_real_request(self):验证 Mock 阻止了真实网络请求responses.get(https://nonexistent-domain-12345.com/api,json{ok:True},status200,)resprequests.get(https://nonexistent-domain-12345.com/api)assertresp.json()[ok]isTrue# Mock 生效没有真实请求responses.activatedeftest_unmatched_url_raises(self):验证未注册的 URL 会触发 ConnectionError# 没有为这个 URL 注册 Mockwithpytest.raises(requests.exceptions.ConnectionError):requests.get(https://api.not-mocked.com/data)4. 项目总结核心知识点Mock 库拦截层面激活方式适用场景responsesurllib3 adapter (全局)responses.activate顶层requests.get()requests-mockSession adapterwith Mocker()通过 Session 调用unittest.mock函数级别patchMock 特定函数返回值优点 缺点对比维度responsesrequests-mock全局拦截✅ 是❌ 仅拦截特定 Session对顶层 API 生效✅❌精细度中URL 匹配高自定义 matcher异常模拟✅ 直接抛异常对象✅ 直接抛异常对象适用场景单元测试所有外部 HTTP 依赖必须 MockCI/CD 流水线零网络依赖毫秒级完成异常场景测试模拟超时、5xx、网络错误本地开发离线环境下也能跑全量测试注意事项Mock 后务必验证 Mock 被调用responses.assert_call_count()测试中不要混用真实请求和 Mock 请求Mock 数据应贴近真实 API 响应格式避免测试失真CI 中确保 Mock 框架版本与 requests 版本兼容常见踩坑经验案例一Mock 未激活但测试通过——假阳性。某测试中responses.activate装饰器被误删但测试仍然通过——因为 CI 环境恰好能访问那个测试 API。三个月后 API 下线测试失败才发现 Mock 从未生效。根因未验证 Mock 是否被调用。修复每个测试末尾加responses.assert_call_count(url, 1)。案例二Mock 数据与真实 API 格式不一致。用{status: ok}做 Mock但真实 API 返回的是{code: 200, data: {...}}。代码在测试中正常运行上线后解析失败。根因Mock 数据与真实格式脱节。修复Mock 数据应从真实 API 文档或抓包获取。案例三requests-mock 对 dependencies 中的 requests.get() 不生效。被测代码依赖一个第三方库该库内部用requests.get()发请求。requests-mock 挂在 Session 上第三方库不走 SessionMock 未生效。根因选错了 Mock 库。修复改用 responses全局拦截或 Mock 第三方库调用。思考题设计题你的系统依赖 10 个第三方 API每个 API 返回格式不同。如何设计一个Mock 数据工厂——能从真实 API 响应的抓包数据HAR 文件/JSON自动生成 Mock 配置减少手动编写 Mock 数据的工作量集成题单元测试和集成测试的边界在哪哪些场景必须用真实请求集成测试/E2E 测试而不是 Mock如何设计 CI 流水线使得 Mock 测试和真实请求测试都能高效运行延伸阅读与资源Milvus向量数据库实战修炼从 0 到 1精通向量检索与生产落地后端工程师的 AI 转型第一课Ollama 与私有化大模型实战10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析

相关新闻

遗传算法工程落地指南:破解早熟收敛与参数玄学

遗传算法工程落地指南:破解早熟收敛与参数玄学

1. 项目概述:为什么“遗传算法第二讲”比第一讲更值得细读“遗传算法”这个词,刚听时容易让人联想到生物课上染色体配对、孟德尔豌豆实验——好像离编程很远,离优化问题更远。但实际在工业界,从芯片布线的功耗最小化,到…

2026/7/13 9:32:27 阅读更多 →
携程景点数据+用户评论自动化采集工具(Python可配置脚本包)

携程景点数据+用户评论自动化采集工具(Python可配置脚本包)

本文还有配套的精品资源,点击获取 简介:一套开箱即用的Python爬虫工具,专注抓取携程平台公开的景点基础信息和真实用户评论。能批量获取景点名称、地址、评分、门票价格、开放时间等POI字段,同时支持分页提取每条景点下的用户名…

2026/7/13 9:32:27 阅读更多 →
C++ for循环深度解析:从基础语法到性能优化与实战陷阱

C++ for循环深度解析:从基础语法到性能优化与实战陷阱

1. 项目概述:为什么for循环是C的基石 如果你刚开始学C,或者从其他语言转过来,可能会觉得 for 循环不就是个基础语法嘛,有什么好讲的?我刚开始也这么想,直到后来在项目里踩了无数坑,才意识到一…

2026/7/13 9:30:24 阅读更多 →

最新新闻

PV操作 7大经典问题:从生产者消费者到理发师,3种死锁场景深度解析

PV操作 7大经典问题:从生产者消费者到理发师,3种死锁场景深度解析

PV操作七大经典问题:从死锁视角解析同步机制设计 引言:同步问题与死锁风险的本质 在多进程/线程编程中,PV操作是解决同步与互斥问题的核心工具。所谓PV操作,指的是对信号量(Semaphore)的两种原子操作&…

2026/7/13 10:18:45 阅读更多 →
Python图像处理库协作原理与工程实践指南

Python图像处理库协作原理与工程实践指南

1. 这不是“学个库就完事”的速成课:为什么图像处理库在Python里从来不是孤立存在你打开Jupyter Notebook,敲下import cv2或from PIL import Image,以为接下来就是调几个函数、加几行滤镜、跑个边缘检测——结果卡在读取路径报错、通道顺序混…

2026/7/13 10:16:45 阅读更多 →
Unity打包后阴影消失?系统排查与解决方案全解析

Unity打包后阴影消失?系统排查与解决方案全解析

1. 项目概述:从编辑器到打包,阴影为何“消失”?如果你是一名Unity开发者,尤其是项目临近发布时,大概率遇到过这个让人头疼的问题:在Unity编辑器里运行得好好的,场景光影氛围感十足,角…

2026/7/13 10:14:44 阅读更多 →
解决UE5启动报错“D3D11兼容GPU”:显卡选择与多GPU环境配置指南

解决UE5启动报错“D3D11兼容GPU”:显卡选择与多GPU环境配置指南

1. 项目概述:当UE5告诉你“显卡不行”刚装好UE5,满心欢喜双击图标,结果一个弹窗直接把你打回现实:“A D3D11-compatible GPU (Feature Level 11.0, Shader Model 5.0) is required to run the engine”。这感觉就像你拿着最新款的…

2026/7/13 10:14:44 阅读更多 →
语音端点检测 VAD 实战:结合音量与过零率双门限的 5 步实现方案

语音端点检测 VAD 实战:结合音量与过零率双门限的 5 步实现方案

语音端点检测实战:基于双门限策略的鲁棒VAD算法实现 1. 语音端点检测的技术挑战与应用价值 在嘈杂的会议室里,智能音箱如何准确识别"小X同学"的唤醒词?在线会议系统怎样自动过滤键盘敲击声只保留人声?这些场景的核心技…

2026/7/13 10:12:43 阅读更多 →
企业微信API二次开发:百万客户朋友圈互动并发,你的对账总线卡壳了吗?

企业微信API二次开发:百万客户朋友圈互动并发,你的对账总线卡壳了吗?

在企业微信 SCRM(私域运营)的终极演进中,企业朋友圈(Enterprise Moments)不仅是进行全域营销种草的橱窗,更是挖掘高价值潜客行为的“黄金雷达”。当总部运营一键向全国 50 万名客户的企微朋友圈推送了双十一…

2026/7/13 10:12:43 阅读更多 →

日新闻

Palworld存档编辑完全掌握:从零开始实现游戏数据可视化修改

Palworld存档编辑完全掌握:从零开始实现游戏数据可视化修改

Palworld存档编辑完全掌握:从零开始实现游戏数据可视化修改 【免费下载链接】palworld-save-tools Tools for converting Palworld .sav files to JSON and back 项目地址: https://gitcode.com/gh_mirrors/pa/palworld-save-tools 你是否曾经想要调整Palwor…

2026/7/13 0:01:19 阅读更多 →
浦东旧模块回收哪家强?专业评测带你一探究竟

浦东旧模块回收哪家强?专业评测带你一探究竟

于科技迅猛飞速迭代的当下此刻, 旧模块的回收处置, 不但关联着资源的再度利用, 而且更牵扯到数据安全以及环保合规事宜。你是不是也正为那堆积得如同山峦般的旧模块而发愁? 是不是不清楚该怎样安全且高效地去处理它们? 别忧心烦恼, 就在今日, 我会以具备权威影响力的自媒体博…

2026/7/13 0:01:19 阅读更多 →
卡梅德生物技术快报|重组蛋白的表达和纯化:IMAC 金属螯合色谱全流程工艺手册|基质 - 配基 - 金属离子匹配与蛋白质分离纯化参数优化

卡梅德生物技术快报|重组蛋白的表达和纯化:IMAC 金属螯合色谱全流程工艺手册|基质 - 配基 - 金属离子匹配与蛋白质分离纯化参数优化

1 研究背景与现存技术痛点(提出问题)基因工程、蛋白质组学、生物制药研发流程中,蛋白质分离纯化是决定下游实验成败的关键环节。当前实验室常规蛋白质分离纯化工艺存在三类难以标准化的技术瓶颈:传统离子交换、分子筛层析无特异性…

2026/7/13 0:05:20 阅读更多 →

周新闻

互联网大厂 Java 求职面试:燕双非的搞笑回答与技术探讨

互联网大厂 Java 求职面试:燕双非的搞笑回答与技术探讨

互联网大厂 Java 求职面试:燕双非的搞笑回答与技术探讨 在一个阳光明媚的上午,互联网大厂的面试官坐在桌前,准备迎接他的面试候选人——燕双非,一个以搞笑和幽默著称的程序员。第一轮提问 面试官:燕双非,作…

2026/7/13 4:38:36 阅读更多 →
车载以太网PMA测试设备选型:示波器、VNA、信号源3类仪器关键参数与预算评估

车载以太网PMA测试设备选型:示波器、VNA、信号源3类仪器关键参数与预算评估

车载以太网PMA测试设备选型:示波器、VNA、信号源3类仪器关键参数与预算评估在智能驾驶和车联网技术快速发展的今天,车载以太网作为新一代车载网络的核心传输技术,其物理层性能直接决定了数据传输的可靠性和稳定性。1000BASE-T1作为当前主流的…

2026/7/13 4:38:38 阅读更多 →
VSCode EIDE 插件 2.0:APM32/STM32 项目迁移实战,5步完成Keil工程转换

VSCode EIDE 插件 2.0:APM32/STM32 项目迁移实战,5步完成Keil工程转换

VSCode EIDE 插件 2.0:APM32/STM32 项目迁移实战指南嵌入式开发领域正经历一场工具链的静默革命。当传统Keil用户首次打开VSCode的扩展市场搜索EIDE时,往往会惊讶于这个看似简单的插件竟能重构十余年的开发习惯。本文将揭示如何用五个精准步骤&#xff0…

2026/7/13 4:38:40 阅读更多 →

月新闻