Python 测试中 mock 和 patch 应该怎么用?
简化版
mock 用来替换测试中的外部依赖,比如网络请求、数据库、时间、第三方 SDK,让测试更稳定、更快。patch 的关键是补丁要打在“被测代码实际查找对象的位置”,而不是对象最初定义的位置。
详细版
mock 常见用途:
- 隔离外部 HTTP 请求;
- 模拟第三方 SDK 返回;
- 固定当前时间;
- 模拟异常场景;
- 验证某个函数是否被调用、调用了几次、参数是什么。
示例:
from unittest.mock import patch
def test_get_user():
with patch("app.service.http_client.get") as mock_get:
mock_get.return_value = {"id": 1}
assert get_user(1) == {"id": 1}
mock_get.assert_called_once()
面试重点是:mock 是为了隔离不稳定依赖,不是为了把所有真实逻辑都替换掉。过度 mock 会让测试只验证 mock 配置是否正确。
完整版教学
一、为什么需要 mock
单元测试应该尽量快、稳定、可重复。如果一个测试每次都真的访问第三方接口、发送短信、写真实数据库,它就会变慢且不稳定。网络会抖动,第三方服务会限流,真实数据库状态会变化。
mock 的作用是把这些不受控的外部依赖替换成可控对象。这样测试可以专注验证被测逻辑本身。
例如订单服务调用支付服务。如果你只是测试“支付成功后订单状态变为已支付”,不需要真的调支付网关,可以把支付网关 mock 成固定返回成功。
二、Mock 对象能做什么
Python 标准库提供 unittest.mock。Mock 对象可以设置返回值:
from unittest.mock import Mock
client = Mock()
client.get_user.return_value = {"id": 1, "name": "Tom"}
assert client.get_user(1)["name"] == "Tom"
client.get_user.assert_called_once_with(1)
它还可以设置副作用,例如抛异常或按顺序返回不同值:
client.get_user.side_effect = TimeoutError("timeout")
这些能力可以帮助我们测试异常分支、重试逻辑、降级逻辑。
三、patch 的核心规则:补使用位置
patch 最容易错的点是路径。补丁应该打在被测代码查找对象的位置,而不是对象定义的位置。
假设代码如下:
# app/payment.py
def charge():
...
# app/order.py
from app.payment import charge
def pay_order():
return charge()
测试 pay_order 时,应该 patch:
patch("app.order.charge")
而不是:
patch("app.payment.charge")
因为 order.py 已经把 charge 导入到自己的命名空间,pay_order 实际查找的是 app.order.charge。
这个细节是 mock 面试题最高频的坑。
四、patch 的几种写法
上下文管理器写法清晰:
def test_pay_order():
with patch("app.order.charge") as mock_charge:
mock_charge.return_value = True
assert pay_order() is True
装饰器写法适合单个测试:
@patch("app.order.charge")
def test_pay_order(mock_charge):
mock_charge.return_value = True
assert pay_order() is True
pytest 中也常用 monkeypatch:
def test_env(monkeypatch):
monkeypatch.setenv("APP_ENV", "test")
monkeypatch 更适合临时修改环境变量、属性、字典项等,测试结束后会自动恢复。
五、mock 外部 HTTP 调用
如果业务函数里调用 HTTP:
def fetch_user(user_id):
resp = requests.get(f"https://api.example.com/users/{user_id}")
return resp.json()
测试时可以 patch requests.get:
def test_fetch_user():
with patch("app.user_service.requests.get") as mock_get:
mock_get.return_value.json.return_value = {"id": 1}
assert fetch_user(1) == {"id": 1}
路径仍然要看 fetch_user 所在模块如何引用 requests。如果被测模块是 app.user_service,通常 patch app.user_service.requests.get。
六、mock 的边界
mock 太少,测试会慢且脆弱;mock 太多,测试会脱离真实行为。好的测试通常会在单元测试中 mock 外部边界,在集成测试中保留更多真实组件。
例如数据库 repository 层可以用测试数据库验证真实 SQL;service 层可以 mock repository 来测试业务分支;API 层可以用测试客户端走完整请求链路。
不要为了让测试通过而 mock 掉被测函数内部所有逻辑。那样测试只是在验证你写的 mock 返回值,不再验证业务代码。
七、常见误区与追问
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 外部 HTTP | mock 请求客户端 | 避免网络抖动和第三方限流 |
| 当前时间 | patch 时间来源 | 固定时间让断言稳定 |
| 数据库 SQL | 集成测试尽量用测试库 | mock 无法发现 SQL 和映射错误 |
| 复杂 SDK | 用 spec/autospec | 防止拼错方法名测试仍通过 |
对象定义位置:app.payment.charge
被测代码导入:from app.payment import charge -> app.order.charge
测试 pay_order 时 patch:app.order.charge
记忆钩子:patch 不问对象最初在哪里定义,只问被测代码运行时从哪里查找它。
第一个误区是 patch 错位置。记住补丁打在被测代码使用对象的位置。
第二个误区是没有验证调用参数。很多场景不仅要验证返回结果,还要验证外部依赖是否用正确参数调用。
第三个误区是 mock 和真实对象接口不一致。可以用 autospec=True 或明确 spec,减少拼错方法名还不报错的问题。
第四个误区是所有测试都 mock 数据库。这样 SQL 错误、事务问题、模型映射问题可能完全测不出来。
- 误区:patch 对象定义位置就一定生效。 如果被测模块已经
from x import y,运行时查找的是被测模块里的名字,要 patch 使用位置。 - 误区:mock 越多测试越稳定。 过度 mock 会让测试脱离真实行为,只验证 mock 配置,关键集成风险反而被遮住。
- 误区:只验证返回值,不验证外部调用参数。 对支付、消息、邮件这类外部依赖,还要验证调用次数、参数和异常处理。
- 追问:
autospec=True有什么用? 它让 mock 尽量遵循真实对象签名和属性,减少拼错方法名、参数不匹配但测试仍通过的风险。 - 追问:
mock和 pytest 的monkeypatch怎么选?mock.patch常用于替换函数、类、方法并断言调用;monkeypatch常用于临时改环境变量、属性、字典项。 - 追问:什么时候不应该 mock 数据库? Repository、SQL 映射、事务行为、迁移兼容性要靠测试数据库或集成测试验证,不能全靠 mock。
八、加强记忆
记 mock 的原则:外部依赖可替换,核心逻辑要保留;patch 要补被测代码实际查找对象的位置;单元测试多隔离,集成测试多真实。mock 是让测试稳定的工具,不是逃避真实验证的遮羞布。