Python mock.patch 没有生效:替换被测模块实际查找的名称
mock.patch 已创建但仍调用真实函数时,沿调用表达式定位名称绑定。区分模块顶部 from import、模块属性读取与函数内导入,核对补丁时间和实际调用。
测试已经进入 patch() 上下文,也拿到了 MagicMock,被测函数却仍调用真实依赖。若代码使用了 from provider import fetch,而测试补丁写成 patch("provider.fetch"),先看调用处查找的是哪个名称。
patch 临时替换一个名称指向的对象。它不会把指向原对象的所有变量一起改掉。本文针对 Python 3 的 unittest.mock,示例里的 app 是可导入的项目包,模块和函数名应替换为实际名称。
from import 会在使用模块里留下独立绑定
假设项目已有 app/provider.py,其中定义 fetch_record(record_id);app/service.py 在模块顶部导入它:
# app/service.py
from app.provider import fetch_record
def load_name(record_id):
record = fetch_record(record_id)
return record["name"]
service 导入完成后,service.fetch_record 指向当时取得的函数。随后替换 app.provider.fetch_record,不会重新执行那条导入语句。load_name() 仍查找 service 自己的全局名称。
因此应补丁 app.service.fetch_record。下面是测试示例,沿用上面的函数接口;返回数据结构需与项目的依赖契约一致:
from unittest.mock import patch
from app import service
def test_load_name_uses_stub():
with patch(
"app.service.fetch_record",
autospec=True,
return_value={"name": "sample"},
) as mocked_fetch:
assert service.load_name("r-1") == "sample"
mocked_fetch.assert_called_once_with("r-1")
autospec=True 按实际函数生成可约束调用签名的 mock;它不能纠正错误的补丁路径。既检查业务返回值,也检查调用参数,才能区别“业务结果碰巧相同”和“确实经过了替身”。这个示例不执行真实依赖,但它只覆盖被替换的调用点,不能保证项目所有外部操作都已隔离。
import 模块时,看调用表达式上的属性
如果 service.py 改用模块导入:
# app/service.py
from app import provider
def load_name(record_id):
return provider.fetch_record(record_id)["name"]
这次调用先取得 provider 模块,再读取它的 fetch_record 属性。替换 app.provider.fetch_record 可以影响这一读取;也可从被测模块明确指向该属性:
from unittest.mock import patch
from app import service
def test_load_name_uses_provider_stub():
with patch.object(
service.provider,
"fetch_record",
autospec=True,
return_value={"name": "sample"},
) as mocked_fetch:
assert service.load_name("r-1") == "sample"
mocked_fetch.assert_called_once_with("r-1")
这里修改的是共享模块对象的属性。在补丁有效期间,同一进程内其他读取该属性的代码也可能受影响;退出上下文后 patch.object 恢复它。不要把这项作用范围误认为只改了 load_name() 内部的一个局部变量。
用真实导入名称,不照搬 main
补丁字符串使用可导入的模块名称,不是磁盘路径。app/service.py 对应的常规名称是 app.service,不能写成 app/service.py.fetch_record。__main__.fetch_record 只适用于该名称确实位于当前入口模块的情况;由 pytest 导入的应用模块通常有自己的包名。
还要区分导入发生的时机。如果函数内部每次执行 from app.provider import fetch_record,读取发生在函数调用时,补丁定义模块在该次导入前生效,就能影响这次绑定。它与前面“模块已导入并保存引用”的情况不同,不应机械地把所有补丁都移到使用模块。
反过来,依赖若早已存入默认参数、闭包、类属性或另一个变量,只补丁模块上的当前名称也可能碰不到那份引用。沿实际调用表达式继续查找,必要时让函数显式接收依赖;不要通过重载模块来反复尝试,重载可能重新触发初始化与副作用。
补丁没被调用时,检查执行路径和有效时间
mocked_fetch.assert_called_once_with(...) 失败,说明该 mock 没记录到预期调用。对照被测函数实际进入的分支、导入别名和缓存结果,再核对调用是否发生在 with patch(...) 内。异步任务或后台线程若等到上下文退出后才调用,名称已经恢复。
路径拼写错误通常应保留为错误。不要加 create=True 去创建一个业务代码从未读取的属性,也不要仅凭 mock 对象存在就认为补丁成功。修改前保留测试原稿;修复后在项目原测试入口检查调用参数、返回值与失败分支,确认没有真实依赖调用。需要回退时恢复补丁目标和相关导入调整,避免只改回其中一处。


