← 返回题目列表

Python 测试中 mock 和 patch 应该怎么用?

高频 中等 第 6 / 27 题 更新于 2026/07/27
mockpatchunittest.mock外部依赖

简化版

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.mockMock 对象可以设置返回值:

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 返回值,不再验证业务代码。

七、常见误区与追问

场景推荐做法原因
外部 HTTPmock 请求客户端避免网络抖动和第三方限流
当前时间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 是让测试稳定的工具,不是逃避真实验证的遮羞布。