为何pytest mocker补丁模块方法的范围因导入方式而异?
关于unittest.mock补丁范围与导入方式的问题解析
问题场景
测试需求:仅补丁主模块中的time.sleep并验证调用,不影响子模块,但实际出现两种不同结果:
- 场景A:各模块通过
import time导入模块,测试中执行mocker.patch("main.time.sleep")时,所有子模块的time.sleep也被补丁,不符合预期; - 场景B:各模块通过
from time import sleep导入方法,测试中执行mocker.patch("main.sleep")时,仅主模块的sleep被补丁,符合预期。
核心疑问
为何导入方式会导致补丁范围差异?场景A中time.sleep为何会被全局补丁?按照unittest.mock的「补丁位置原则」,补丁是临时修改名称指向的对象,为何两种场景的补丁范围不同?
核心原理:补丁的「查找位置」规则
unittest.mock的补丁本质是临时修改名称到对象的绑定关系,关键遵循「补丁对象被查找的位置,而非定义位置」的规则。而Python中模块导入的两种方式,会导致名称绑定的位置完全不同:
- 当使用
import time时,所有导入该模块的文件,都会共享同一个time模块对象——这个对象是全局唯一的,存放在Python的模块缓存中; - 当使用
from time import sleep时,每个导入的模块都会在自己的命名空间里创建一个sleep名称,直接指向原time.sleep函数对象,和其他模块的sleep名称是独立的引用。
场景A(import time)的补丁逻辑
在场景A中:
- main.py和module.py都通过
import time获取了同一个全局time模块对象的引用; - 测试中执行
mocker.patch("main.time.sleep"),实际修改的是全局time模块对象的sleep属性; - 当main.py调用
time.sleep、module.py调用time.sleep时,都是从同一个全局time模块对象中查找sleep,自然都会使用被补丁后的mock对象,所以最终调用次数是2次,导致断言失败。
对应代码示例:
main.py
import time import module def sleep_in_main(): time.sleep(1) # 被补丁 module.sleep_in_module()
module.py
import time def sleep_in_module(): time.sleep(1) # 也被补丁
test.py
import main def test(mocker): sleep_mock = mocker.patch("main.time.sleep") main.sleep_in_main() assert sleep_mock.call_count == 1 # 失败(实际被调用2次)
场景B(from time import sleep)的补丁逻辑
在场景B中:
- main.py在自己的命名空间里创建了
sleep名称,指向原time.sleep;module.py也在自己的命名空间里创建了独立的sleep名称,同样指向原time.sleep; - 测试中执行
mocker.patch("main.sleep"),仅修改了main模块命名空间里的sleep名称的绑定,把它指向mock对象; - module.py里的
sleep名称依然指向原time.sleep函数,所以调用module.sleep_in_module()时不会触发mock,最终main里的sleep只被调用1次,断言通过。
对应代码示例:
main.py
from time import sleep import module def sleep_in_main(): sleep(1) # 被补丁 module.sleep_in_module()
module.py
from time import sleep def sleep_in_module(): sleep(1) # 未被补丁
test.py
import main def test(mocker): sleep_mock = mocker.patch("main.sleep") main.sleep_in_main() assert sleep_mock.call_count == 1 # 通过
总结
- 使用
import 模块导入时,补丁模块的属性会影响所有共享该模块对象的文件; - 使用
from 模块 import 方法导入时,补丁当前模块的方法名称仅影响当前模块的引用,不会波及其他模块; - 若想在场景A中实现仅补丁main模块的
time.sleep,可以考虑补丁main.time这个模块引用本身(而非模块属性),但更简洁的方式是调整为场景B的导入方式。
内容的提问来源于stack exchange,提问作者Kuhkäfer
相关产品推荐
相关产品推荐

