Python mock.patch 补丁位置:函数明明来自这里,为什么替换后仍没有生效

10-01 3阅读

替换了函数,调用方却没有改变

给代码写测试时,经常想把读取接口替换成一个可控结果。patch 没有报错,替身也创建成功,业务函数却仍返回原值,问题可能出在名称查找位置。函数从哪个文件定义出来,与执行到调用语句时从哪里取得它,是两个不同问题。先找到调用方真正读取的名称,才能选对补丁目标。

示例在 Python 3.12.14 上执行,只使用标准库,保存为 demo.py 即可运行。它在临时目录写出两个极小模块:supplier18 提供 fetch,consumer18 使用 from supplier18 import fetch,然后在 label 中调用这个已绑定名称。模拟对象始终返回固定字符串,不访问网络或真实业务服务。

给两个可能的位置各补一次

首次导入 consumer18 时,fetch 名称就绑定在 consumer18 的模块字典里。此后仅把 supplier18.fetch 换成替身,并不会追踪并改写已经复制到其他命名空间的绑定。第一段上下文故意补到供应方,断言替身一次也没被调用;第二段补到使用方,才检查预期结果和准确调用次数。

Python mock.patch 补丁位置:函数明明来自这里,为什么替换后仍没有生效

AI概念示意图:依赖已经从供应工具箱进入使用工具箱,替换应落在实际取用的位置。图片是概念说明,不是测试截图。

import importlib
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch

with tempfile.TemporaryDirectory() as folder:
    root = Path(folder)
    (root / "supplier18.py").write_text(
        "def fetch():\n    return 'real'\n", encoding="utf-8")
    (root / "consumer18.py").write_text(
        "from supplier18 import fetch\n"
        "def label():\n    return fetch()\n", encoding="utf-8")
    sys.path.insert(0, folder)
    try:
        consumer = importlib.import_module("consumer18")
        with patch("supplier18.fetch", return_value="fake") as mock:
            result = consumer.label()
            assert result == "real"
            mock.assert_not_called()
            print("supplier patch:", result)
        with patch("consumer18.fetch", return_value="fake") as mock:
            result = consumer.label()
            assert result == "fake"
            mock.assert_called_once_with()
            print("consumer patch:", result)
        assert consumer.label() == "real"
        print("restored:", consumer.label())
    finally:
        sys.path.remove(folder)
        sys.modules.pop("consumer18", None)
        sys.modules.pop("supplier18", None)

把没有生效也写成可验证结果

输出是 supplier patch: real、consumer patch: fake、restored: real。第一行不是 patch 失效,而是业务函数没有查那个位置。第二行对应真正的依赖替换。最后一行证明退出 with 之后,原有绑定已经恢复;因此测试中的临时替换不会在这个正常退出路径上继续影响后续调用。

本例同时使用 assert_not_called 与 assert_called_once_with,避免只检查返回字符串。若实现意外多调用一次,或者调用时传入了新参数,仅凭 fake 这个结果发现不了问题。具体测试还可以为替身设置异常,验证调用方失败路径,但每个断言应服务于业务行为,而不是把所有内部步骤都固定死。

导入写法改变,目标也要重新判断

如果 consumer18 改用 import supplier18,再通过 supplier18.fetch 调用,它会在运行时读取供应模块的属性,此时补丁位置就要按新的查找路径审视。不要把“永远补调用模块”背成不带条件的规则;应从实际调用表达式出发,逐级确认名称指向哪个对象。

默认参数或闭包中已经保存的函数引用,也可能绕过后来修改的模块属性。遇到这种情况,应先理解引用何时被保存,再考虑改造为显式传入依赖。patch 适合在测试范围内暂时替换已有入口,并不证明替身与真实接口永远一致。对接口形状有要求时,可以另用 autospec 约束调用;本实验只专注名称查找,便于看清失败原因。

迁移到自己的项目时,先打开被测函数,找到实际调用表达式,再查看它所在模块的导入语句。把补丁字符串写成真实模块路径,而不是示例的临时名称;同时让替身返回一个与真实结果明显不同的值。若断言仍不通过,优先查看替身是否被调用,再检查结果处理逻辑。这样能区分“压根没走到替身”和“已经走到,但调用方又加工了返回值”,避免在错误层次反复调整测试数据。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。