在 Pytest 测试体系中,Mock 补丁用来替换掉外部依赖,比如网络请求、数据库查询或时间函数,从而让用例稳定可重复。但补丁一旦没有在用例结束时被正确撤掉,它就会留在 Python 模块的属性上,使之后运行的完全不相关的测试也使用被替换的对象。这种跨测试污染通常不会让代码报错,却会让测试结果失去意义,甚至在并行执行时表现为偶发失败,极难排查。

理解 Mock 补丁的生命周期与污染原理
当我们使用 unittest.mock 中的 patch 函数时,它本质上是在目标模块上临时替换某个属性,比如把 requests.get 换成一个 Mock 对象。补丁的生效依赖于 start 和 stop 两个动作:调用 patch.start() 之后,目标被替换;只有调用 patch.stop() 或退出 with 上下文,原对象才会恢复。如果测试中抛了异常或者忘记写 stop,补丁就会残留在 sys.modules 缓存的模块里。
跨测试污染的常见场景是开发者在模块顶层或 conftest 中用 patch 做了全局替换,却没有限定作用范围。例如在一个测试文件里给 time.time 打补丁返回固定值,另一个文件假设时间是真实流逝的,两者若在同一进程按顺序跑,后者就会拿到假时间。下面这段代码展示了危险的写法:
import time
from unittest import mock
# 错误示范:在模块导入时打补丁且未停止
mock.patch('time.time', return_value=1000.0).start()
def test_a():
assert time.time() == 1000.0
# 其他测试文件里的用例也会受影响
上面的代码在导入阶段就启动了补丁,且永远不停止,等同于给整个测试进程植入了假时间。要避免这类问题,必须明确补丁的边界,让它只活在单个用例或一组相关用例内部,并在退出时自动还原。
使用 Pytest Fixture 控制补丁作用域
Pytest 的 fixture 机制是管理 Mock 补丁最干净的方式。通过把 patch 放进 fixture 的 with 块中,并利用 yield 把 Mock 对象传给测试,Pytest 会在 fixture 结束后自动执行退出逻辑,补丁随之撤销。fixture 的 scope 参数决定了污染范围:函数级(默认)每次用例都重建,隔离性最好;类级在同一测试类内共享;会话级则最容易引发跨文件污染。
下面示例用函数级 fixture 安全地替换一个外部 API 调用,用例结束补丁立刻消失,不会影响其他测试:
import pytest
from unittest import mock
import myapp.services as services
@pytest.fixture
def fake_fetch(mocker):
with mock.patch('myapp.services.fetch_remote') as mocked:
mocked.return_value = {'ok': True}
yield mocked
def test_service_ok(fake_fetch):
result = services.process()
assert result['status'] == 'ok'
fake_fetch.assert_called_once()
如果确实需要在多个用例间共享同一个 Mock,可以把 scope 设为 class 或 module,但一定要确认这些用例逻辑上属于同一上下文。对于绝大多数业务测试,坚持函数级 fixture 能把跨测试污染概率降到最低。此外,pytest-mock 提供的 mocker fixture 已经封装了自动清理,即便用例报错也会还原,比手写 start/stop 更可靠。
借助 pytest-mock 与上下文管理器彻底隔离
pytest-mock 插件的 mocker fixture 是对 unittest.mock 的轻量包装,它的最大价值在于测试结束时无条件撤掉所有通过它建立的补丁。即便测试因为断言失败提前退出,mocker 的终结逻辑依然运行,因此不会出现“挂着的”替换。相比直接用 mock.patch 全局函数,它更贴合 Pytest 的生命周期。
除了 fixture,显式 with mock.patch(...) 语句也是隔离补丁的好办法。它的作用域就是缩进块,离开块立刻还原,非常适合只在一小段逻辑里需要假数据的场景。下面的例子在用例内部用上下文管理器替换配置读取,离开 with 后配置函数恢复正常:
from unittest import mock
import myapp.config as config
def test_feature_flag_off():
with mock.patch('myapp.config.get_flag', return_value=False):
assert config.is_enabled('new_ui') is False
# 此处补丁已失效
assert config.get_flag('new_ui') is True
当项目里同时存在手动 patch.start 和 fixture 时,建议统一收敛到 mocker 或 with 语句,减少认知负担。团队可以在代码评审清单中加上一条“禁止在模块顶层调用 patch.start”,从规范上杜绝跨测试污染。配合 pytest -p no:cacheprovider 与并行执行插件,也能更快暴露那些依赖隐藏状态的脆弱用例,倒逼补丁管理走向严谨。
PytestMock_patchtest_isolation修改时间:2026-08-14 02:15:26