bigywater.tech
Python · 38

Mock 原理:为什么要 patch 使用它的地方

真实对比from external import fetch和import external两种写法,验证unittest.mock.patch到底该填哪个路径才能真的生效,两种写法给出的答案完全相反。

from...import场景
真实必须patch使用方(service),不是定义方(external)
import模块场景
真实结论完全反过来,要patch定义方
判断依据
真实看调用那一刻沿哪条命名空间路径查找

做的事:同一个被mock的函数,用两种真实不同的import方式引用

代码 external.py / service.py / service2.py# external.py def fetch(): return "REAL_DATA_FROM_NETWORK" # service.py —— from ... import 具体名字 from external import fetch def get_report(): return f"report: {fetch()}" # service2.py —— import 整个模块 import external def get_report(): return f"report: {external.fetch()}"

验证:对service.py,patch('external.fetch')真实完全没用

实测 · 本机执行输出
with mock.patch("external.fetch", return_value="FAKE"):
    service.get_report()
→ report: REAL_DATA_FROM_NETWORK    ← 真实patch没生效,还是走了真实的fetch()

with mock.patch("service.fetch", return_value="FAKE"):
    service.get_report()
→ report: FAKE                       ← 真实patch生效了
这正是本文标题这句话的真实来源:"patch使用它的地方,不是patch定义它的地方"。service.py里写的是from external import fetch——这行代码执行完之后,service模块自己的命名空间里真实多了一个叫fetch的名字,它跟external.fetch此刻指向同一个函数对象,但它们是两个真实独立的"引用"(就像本系列前面讲闭包和自由变量时验证过的,名字绑定和对象本身是两件事)。mock.patch("external.fetch", ...)真实只改写了external模块命名空间里fetch这个名字指向的对象,service模块自己命名空间里那个独立的fetch名字完全没受影响,service.get_report()调用的正是service自己那份没被改写的引用。要让mock生效,真实必须改写service.fetch这个名字本身——也就是"用到它的地方"的命名空间,而不是"定义它的地方"。

验证:对service2.py,同一个patch字符串的结论真实完全反过来

实测 · 本机执行输出
with mock.patch("external.fetch", return_value="FAKE"):
    service2.get_report()
→ report: FAKE                        ← 真实生效了!跟上面service.py的结论正好相反

with mock.patch("service2.fetch", return_value="FAKE", create=True):
    service2.get_report()
→ report: REAL_DATA_FROM_NETWORK      ← 真实完全没用——因为service2的命名空间里根本没有fetch这个名字

区别在于service2.py写的是import external(拿整个模块对象),调用的时候真实是external.fetch()——这个调用在执行的那一刻,才真的去查external模块的命名空间找fetch这个名字,service2自己的命名空间里从来就没有一个独立的fetch名字(这也是为什么第二个patch要加create=True才不会因为"属性原本不存在"直接报错,即便加了,效果也是patch了一个service2从来不会用到的属性)。所以mock.patch("external.fetch", ...)这次真实改到了point,因为service2查找fetch的路径,真实经过的正是external这个模块对象本身。

真实的判断依据:看被测代码的import写法,不是看函数定义在哪

这组对比给出的真实结论很干脆:mock.patch该填哪个路径字符串,唯一要看的是被测代码在执行到调用那一行时,真实沿着哪条命名空间路径去查找这个名字——from external import fetch把查找结果在import那一刻就固化成了service自己的一个名字,之后每次调用都不会再去看external;import external则是把查找推迟到真正调用external.fetch()的那一刻,每次都真实经过external模块对象。这跟函数fetch本身定义在哪个文件里完全无关,官方文档里"patch where it's looked up, not where it's defined"这句话,本文这次用两种真实对照的import写法,验证出的正是这句话字面的含义——同一个目标函数,因为调用方引用它的方式不同,正确的mock路径真实可能完全不一样,甚至完全相反。

分步演示:from-import和import-module两种场景下的真实patch结果

每一步都是本文上面用真实的unittest.mock跑出来的原始结果。

同一个目标函数,两种import写法下正确的patch路径完全相反
这一步新验证的场景

    本文用到的工具

    • 本机 Python 3.14.7,标准库 unittest.mock
    京ICP备2026067502号-1京公网安备11010502063487号