如何用pytest-mock实现模块间的链式Mock?
链式调用场景下pytest-mock的正确实现方法
问题背景
在编写单元测试时使用mock技术,遇到模块链式调用场景下mock失效的问题。示例代码中,A.py定义的write_to_file被B.py的函数调用,B的函数又被C.py的feature函数调用,测试时mockwrite_to_file失败。更通用的场景是多层嵌套调用(A→B→C→...→N),需要找到正确的mock方式。
示例代码如下:
# file A.py def write_to_file(message, file="test.txt"): with open(file, "w") as fh: fh.write(message) # File B.py from A import write_to_file def part_of_a_big_feature(): # Do some things write_to_file("bla") # Do some things # File C.py(原示例中误写为feature.py) from B import part_of_a_big_feature def feature(): # First part is for a specific feature # Second: I called the part_of_a_big_feature part_of_a_big_feature() # Then another feature # Then return return 1 # file test_my_function.py import pytest from C import feature class TestException(Exception): pass class TestFeature: @staticmethod def setup_method(): pass @staticmethod def teardown_method(): pass @staticmethod def test_feature(mocker): # Mocking路径错误,导致失效 mocker.patch("path.to.A.Bwrite_to_file", return_value=None) # Tests assert feature() == 1
运行命令:pytest -vv test_my_function.py
核心原理
Python的import机制会将目标对象复制到当前模块的命名空间中。比如B.py中from A import write_to_file,相当于在B模块的命名空间里创建了一个指向A.write_to_file的引用,后续B中的函数调用的是这个本地引用,而非A模块里的原函数。因此,mock的关键是针对对象被实际调用的位置(即所在模块的本地引用)进行mock,而非对象的定义位置。
针对示例的修正
示例中,part_of_a_big_feature调用的是B模块本地的write_to_file引用,所以正确的mock路径应该是B.write_to_file(或完整项目路径,比如my_project.B.write_to_file)。
修正后的测试代码:
@staticmethod def test_feature(mocker): # 正确mock路径:B模块中的write_to_file引用 mock_write = mocker.patch("B.write_to_file", return_value=None) assert feature() == 1 # 可选:验证mock函数确实被调用 mock_write.assert_called_once_with("bla")
通用多层嵌套场景解决方案
不管链式调用嵌套多少层,只需遵循以下步骤:
- 定位调用链中直接调用目标函数的那个函数所在的模块
- 根据该模块的导入方式确定mock路径:
- 如果是
from 原模块 import 目标函数导入,mock路径为当前模块名.目标函数 - 如果是
import 原模块后用原模块.目标函数调用,mock路径为当前模块名.原模块.目标函数
- 如果是
- 使用
mocker.patch()对上述路径进行mock
举个通用例子:
- A.py定义
test() - B.py通过
from A import test导入,在function_that_use_test()中调用test() - C.py通过
from B import function_that_use_test导入,在function_that_use_function_that_use_test()中调用该函数
此时,test()实际被调用的位置是B模块的命名空间,所以mock路径应为B.test(或完整模块路径)。
内容的提问来源于stack exchange,提问作者PicxyB
相关产品推荐
相关产品推荐

