用 pytest-mock 模拟第三方 API 完全可行,但必须绕开其 mocker fixture 对 requests 或 httpx 的浅层 patch,真正稳定的做法是结合 requests_mock 或 httpx.MockTransport 处理 HTTP 层,再用 pytest-mock 处理业务层逻辑。

直接说结论:用 pytest-mock 模拟第三方 API 完全可行,但必须绕开它自带的 mocker fixture 对 requests 或 httpx 的“浅层 patch”,否则会漏掉底层连接、重试、会话复用等真实行为——真正稳定的做法是结合 requests_mock 或 httpx.MockTransport,再用 pytest-mock 处理业务层逻辑。
为什么不能只靠 mocker.patch 替换 requests.get
常见错误是这样写:
def test_fetch_user(mocker):
mocker.patch("requests.get", return_value=Mock(json=lambda: {"id": 1}))
assert fetch_user(123)["id"] == 1
问题在于:requests.get 只是顶层封装,实际调用链可能经过 Session.send、HTTPAdapter.send,甚至自定义中间件。一旦代码里用了 requests.Session() 或设置了 timeout/headers,这种 patch 就失效;更糟的是,它无法模拟网络超时、连接拒绝、重定向跳转等真实异常场景。
- mock 对象不校验参数(比如 URL 路径、query string),容易掩盖拼接 bug
- 无法控制响应状态码、headers、stream 行为,导致测试覆盖不全
- 如果被测函数内部重试了 3 次,你得手动配置
side_effect返回多个值,易出错
用 requests_mock 替代 patch,精准控制 HTTP 层
这是最常用且稳定的方案,尤其适合 requests 用户。它在 urllib3 底层拦截请求,行为与真实网络一致,支持状态码、headers、延迟、重复匹配等。
立即学习“Python免费学习笔记(深入)”;
安装并注册 fixture:
pip install requests-mock
在 conftest.py 中注册:
import pytest import requests_mock <p>@pytest.fixture def reqmock(): with requests_mock.Mocker() as m: yield m
测试中使用:
def test_fetch_user_with_error(reqmock):
reqmock.get("https://www.php.cn/link/bb1a58619514c1c544f8972fac9f5717", status_code=404)
with pytest.raises(UserNotFoundError):
fetch_user(123)
<p>def test_fetch_user_with_delay(reqmock):
reqmock.get("<a href="https://www.php.cn/link/bb1a58619514c1c544f8972fac9f5717">https://www.php.cn/link/bb1a58619514c1c544f8972fac9f5717</a>", json={"id": 123}, complete_qs=True)</p><h1>注意:complete_qs=True 才校验 query 参数是否完全一致</h1><pre class="brush:php;toolbar:false;">assert fetch_user(123, include_profile=True)["id"] == 123
- 所有请求 URL 必须显式注册,未注册的请求会抛
ConnectionError,避免漏测 -
complete_qs=True防止因参数顺序不同导致误匹配 - 用
reqmock.register_uri("GET", ...)可注册多次响应,模拟重试逻辑
对 httpx 用户:优先用 httpx.MockTransport,而非 pytest-mock
httpx 从 0.23+ 提供原生 mock 支持,比 patch 更轻量、更可靠。不要试图 patch httpx.get 或 client 实例方法——它们是异步/同步双模式,patch 容易错配。
正确做法:
import httpx
from httpx import MockTransport, Response
<p>def test_fetch_with_httpx():
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/users/456":
return Response(200, json={"id": 456})
return Response(404)</p><pre class="brush:php;toolbar:false;">transport = MockTransport(handler)
client = httpx.Client(transport=transport)
# 或 async_client = httpx.AsyncClient(transport=transport)
assert fetch_user_httpx(456, client=client)["id"] == 456
- handler 函数可访问完整
request对象,包括 method、headers、body,便于验证请求构造是否正确 - MockTransport 不依赖 monkey patch,无并发干扰风险,适合 async 测试
- 若项目混合 sync/async,建议统一用
httpx.Client+ transport,避免多套 mock 逻辑
什么时候才该用 pytest-mock 的 mocker?
它的价值不在 HTTP 模拟,而在隔离业务逻辑依赖:比如第三方 SDK 的类实例、认证 token 管理器、本地缓存 wrapper、或非 HTTP 的外部调用(如 boto3、redis-py)。
def test_with_auth_wrapper(mocker):
mock_token = mocker.Mock()
mock_token.get_token.return_value = "abc123"
mocker.patch("myapp.auth.TokenManager", return_value=mock_token)
<pre class="brush:php;toolbar:false;">result = fetch_protected_data()
assert result["auth_header"] == "Bearer abc123"
- 只 mock 明确知道不会触发网络 I/O 的对象,比如已初始化的 SDK client 实例
- 避免 patch 模块级函数(如
boto3.client),改 patch 具体实例或传入 client 参数做依赖注入 - mock 返回值尽量用真实类型(如
types.SimpleNamespace),别全用Mock(),否则 type-checker 和 IDE 提示失效
真正的难点不在怎么 mock,而在于判断哪一层该 mock、哪一层该真实集成。HTTP 层用专用 mock 库(requests_mock / httpx.MockTransport),业务胶水层用 pytest-mock,边界模糊时宁可多写一行 setup,也不要赌 patch 能覆盖所有调用路径。


















