
在python中使用`pytest-mock`模拟常量时,直接修改源模块的常量可能无法生效,因为`from … import const`会创建常量引用的本地副本。本文将深入解释python的导入机制如何影响`mocker.patch`的行为,并提供两种有效的解决方案:一是直接在调用常量函数所在的模块命名空间中打补丁,二是推迟目标函数的导入,直至常量打补丁操作完成之后。
在Python单元测试中,我们经常需要模拟(mock)外部依赖或常量,以确保测试的隔离性和可预测性。当涉及到模拟一个从其他模块导入的常量时,pytest-mock(基于unittest.mock)的行为有时可能出乎意料。本文将通过一个具体的案例,详细解析为何直接对源模块的常量进行打补丁操作可能无效,并提供两种正确的模拟策略。
理解Python的导入机制与mocker.patch
考虑以下模块结构:
mod1├── mod2│ ├── __init__.py│ └── utils.py└── tests └── test_utils.py
其中文件内容如下:
mod1/mod2/__init__.py:
立即学习“Python免费学习笔记(深入)”;
CONST = -1
mod1/mod2/utils.py:
from mod1.mod2 import CONSTdef mod_function(): print(CONST)
mod1/tests/test_utils.py:
from mod1.mod2.utils import mod_functionimport pytest_mock # 通常由pytest自动注入mocker fixturedef test_mod_function_initial_attempt(mocker): mock = mocker.patch("mod1.mod2.CONST") mock.return_value = 1000 mod_function() # 预期输出1000,实际输出-1
当我们运行pytest并执行test_mod_function_initial_attempt时,会发现mod_function依然打印出-1,而非预期的1000。这是因为Python的导入机制以及mocker.patch的工作原理。
商汤商量
商汤科技研发的AI对话工具,商量商量,都能解决。
36 查看详情
当mod1/mod2/utils.py执行from mod1.mod2 import CONST时,它实际上是在mod1.mod2.utils模块的命名空间中创建了一个名为CONST的新引用,这个引用指向了mod1.mod2.__init__模块中当前CONST变量所指向的-1这个整数对象。
随后,在test_mod_function_initial_attempt中,mocker.patch(“mod1.mod2.CONST”)的作用是将mod1.mod2模块对象的CONST属性修改为一个Mock对象。然而,这并不会影响到mod1.mod2.utils模块中已经存在的那个名为CONST的引用。mod1.mod2.utils.CONST仍然指向原始的-1。因此,mod_function在执行时,访问的是mod1.mod2.utils命名空间中的CONST,而非mod1.mod2中已被打补丁的CONST。
正确模拟常量的策略
要成功模拟一个常量,我们需要确保在被测试的代码(例如mod_function)访问该常量时,它能够看到我们打的补丁。这可以通过两种主要策略实现。
策略一:在常量被引用的命名空间中打补丁
最直接有效的方法是,在常量被实际使用的模块的命名空间中对其进行打补丁。在我们的例子中,mod_function在mod1.mod2.utils模块中查找CONST。因此,我们应该直接修改mod1.mod2.utils模块的CONST属性。
# mod1/tests/test_utils.py (修正后的测试代码)from mod1.mod2.utils import mod_functiondef test_mod_function_patch_in_consumer(mocker): # 直接在mod1.mod2.utils模块中打补丁 mock = mocker.patch("mod1.mod2.utils.CONST") mock.return_value = 1000 mod_function() # 此时将输出 1000
解释:通过mocker.patch(“mod1.mod2.utils.CONST”),我们直接修改了mod1.mod2.utils模块中的CONST引用,使其指向一个Mock对象。当mod_function被调用时,它会从自己的命名空间(即mod1.mod2.utils)中查找CONST,此时找到的就是我们打补丁后的Mock对象,因此print(CONST)会触发Mock对象的行为,从而输出1000。
策略二:推迟导入直到补丁完成
另一种方法是确保在目标函数(mod_function)被导入(从而其内部的CONST引用被建立)之前,源模块的常量已经被打上了补丁。
# mod1/tests/test_utils.py (另一种修正后的测试代码)# 注意:这里不再在文件顶部导入mod_function# from mod1.mod2.utils import mod_functiondef test_mod_function_defer_import(mocker): # 先在源模块mod1.mod2中打补丁 mock = mocker.patch("mod1.mod2.CONST") mock.return_value = 1000 # 然后再导入mod_function。此时mod1.mod2.CONST已经是Mock对象 # 因此mod_function导入时,其内部的CONST将引用这个Mock对象 from mod1.mod2.utils import mod_function mod_function() # 此时也将输出 1000
解释:在这个策略中,我们首先通过mocker.patch(“mod1.mod2.CONST”)将mod1.mod2模块中的CONST属性替换为一个Mock对象。然后,当from mod1.mod2.utils import mod_function语句执行时,mod1.mod2.utils模块内部的from mod1.mod2 import CONST语句会查找mod1.mod2模块中的CONST。此时,它找到的是我们已经设置好的Mock对象,并将其引用到mod1.mod2.utils.CONST。因此,mod_function在执行时,同样会访问到打补丁后的Mock对象。
总结与注意事项
理解Python的导入机制至关重要。 from X import Y会在当前模块的命名空间中创建一个指向X.Y所指向对象的引用。一旦这个引用建立,修改X.Y并不会自动更新当前模块的Y。“在哪里查找,就在哪里打补丁。” 这是unittest.mock(和pytest-mock)的一个核心原则。如果一个函数在module_a中查找CONST,那么你就应该打补丁module_a.CONST,而不是module_b.CONST(即使module_a.CONST最初是从module_b导入的)。推迟导入是一种强大的技术,尤其是在需要模拟模块级别变量或在模块初始化时就发生的事情时。但它也可能使代码结构略显复杂,需权衡使用。
在实际开发中,推荐优先使用策略一,即在常量被引用的命名空间中打补丁,因为它通常更直观且不易出错。只有当常量在模块加载时就被使用,或者存在循环导入等复杂场景时,才考虑使用策略二。通过掌握这些技巧,您可以更有效地在Python中进行单元测试,确保代码的质量和可靠性。
以上就是pytest-mock:深入理解Python常量模拟的正确姿势的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/584908.html
微信扫一扫
支付宝扫一扫