如何模拟 Python 函数,使其在导入过程中不会被调用?

问题描述 投票:0回答:2

我正在为别人的代码编写一些单元测试(使用pytest),我不允许以任何方式更改或改变。这段代码有一个全局变量,它是用任何函数外部的函数返回来初始化的,并且它调用一个函数(在本地运行时)会引发错误。我无法共享该代码,但我编写了一个具有相同问题的简单文件:

def annoying_function():
    '''Does something that generates exception due to some hardcoded cloud stuff'''
    raise ValueError() # Simulate the original function raising error due to no cloud connection


annoying_variable = annoying_function()



def normal_function():
    '''Works fine by itself'''
    return True

这是我的测试功能:

def test_normal_function(): 
    from app.annoying_file import normal_function

    assert normal_function() == True

由于

ValueError
中的
annoying_function
而失败,因为它在模块导入期间仍然被调用。

这是堆栈跟踪:

failed: def test_normal_function():
    
>       from app.annoying_file import normal_function

test\test_annoying_file.py:6: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app\annoying_file.py:6: in <module>
    annoying_variable = annoying_function()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    def annoying_function():
        '''Does something that generates exception due to some hardcoded cloud stuff'''
>       raise ValueError()
E       ValueError

app\annoying_file.py:3: ValueError

我试过像这样嘲笑这个

annoying_function

def test_normal_function(mocker):
    mocker.patch("app.annoying_file.annoying_function", return_value="foo")
    from app.annoying_file import normal_function

    assert normal_function() == True

但结果是一样的。

这是堆栈跟踪:

failed: thing = <module 'app' (<_frozen_importlib_external._NamespaceLoader object at 0x00000244A7C72FE0>)>
comp = 'annoying_file', import_path = 'app.annoying_file'

    def _dot_lookup(thing, comp, import_path):
        try:
>           return getattr(thing, comp)
E           AttributeError: module 'app' has no attribute 'annoying_file'

..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1238: AttributeError

During handling of the above exception, another exception occurred:

mocker = <pytest_mock.plugin.MockerFixture object at 0x00000244A7C72380>

    def test_normal_function(mocker):
>       mocker.patch("app.annoying_file.annoying_function", return_value="foo")

test\test_annoying_file.py:5: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv\lib\site-packages\pytest_mock\plugin.py:440: in __call__
    return self._start_patch(
.venv\lib\site-packages\pytest_mock\plugin.py:258: in _start_patch
    mocked: MockType = p.start()
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1585: in start
    result = self.__enter__()
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1421: in __enter__
    self.target = self.getter()
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1608: in <lambda>
    getter = lambda: _importer(target)
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1251: in _importer
    thing = _dot_lookup(thing, comp, import_path)
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1240: in _dot_lookup
    __import__(import_path)
app\annoying_file.py:6: in <module>
    annoying_variable = annoying_function()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    def annoying_function():
        '''Does something that generates exception due to some hardcoded cloud stuff'''
>       raise ValueError()
E       ValueError

app\annoying_file.py:3: ValueError

移动导入语句也不会影响我的结果。

从我读到的内容来看,发生这种情况是因为模拟者(我正在使用 pytest-mock)必须导入带有它正在模拟的函数的文件,并且在导入此文件期间,行

annoying_variable = annoying_function()
运行,结果失败模拟过程。

我发现完成此类工作的唯一方法是模拟在原始代码中导致错误的云内容,但我想避免这种情况,因为我的测试不再是单元测试。

再次强调,我无法修改或更改原始代码。如果有任何想法或建议,我将不胜感激。

python mocking pytest
2个回答
0
投票

这里有一种非正统且混乱的方法来解决您的问题。它基于以下想法:

  1. 在导入之前
    动态调整
    annoying_module.py的源代码,这样
    annoying_function()
    就不会被调用。根据您的代码示例,我们可以通过在实际源代码中将
    annoying_variable = annoying_function()
    替换为
    annoying_variable = None
    来实现这一点。
  2. 导入动态调整的模块而不是原来的模块。
  3. 在动态调整模块中测试
    normal_function()

在下面的代码中,我假设

  1. 一个名为
    annoying_module.py
    的模块包含您问题中的
    annoying_function()
    annoying_variable
    normal_function()
  2. annoying_module.py
    和包含以下代码的模块都位于同一文件夹中。
from ast import parse, unparse, Assign, Constant
from importlib.abc import SourceLoader
from importlib.util import module_from_spec, spec_from_loader

to_patch = "annoying_module"

def patch_annoying_variable_in(module_name: str) -> str:
    """Return patched source code, where `annoying_variable = None`"""
    with open(f"{module_name}.py", mode="r") as f:
        tree = parse(f.read())
        for s in tree.body:
            # Assign None to `annoying_variable`
            if isinstance(s, Assign) and "annoying_variable" in (t.id for t in s.targets):
                s.value = Constant(value=None)
        return unparse(tree)
    
def import_from(module_name: str, source_code: str):
    """Load and return a module that has the given name and holds the given code."""
    # Following  https://stackoverflow.com/questions/62294877/
    class SourceStringLoader(SourceLoader):
        def get_data(self, *args, **kwargs): return source_code.encode("utf-8")
        def get_filename(self, fullname): return ""
    spec = spec_from_loader(module_name, SourceStringLoader())
    mod = module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod
        
def test_normal_function():   
    patched_code = patch_annoying_variable_in(to_patch)
    mod = import_from(to_patch, patched_code)
    assert mod.normal_function() == True

代码实现了以下功能:

  • patch_annoying_variable_in()
    ,解析
    annoying_module
    的原始代码。对
    annoying_variable
    的赋值被替换,这样
    annoying_function()
    就不会被执行。返回调整后的源代码。
  • 使用
    import_from()
    ,加载调整后的源代码。
  • 最后
    test_normal_function()
    利用前面两个函数来测试动态调整的模块。

更新:我应该明确表示,实际上我不建议以这种方式解决您的问题。正如其他评论者已经指出的那样,您尝试解决的问题暗示了要测试的代码存在更大的问题。


-1
投票

您似乎面临着模拟在模块导入期间执行的函数(annoying_function)的挑战。出现此问题的原因是在模块已导入之后进行了模拟,因此调用了该函数,从而导致错误。

解决此问题的一种方法是使用unittest.mock库而不是pytest-mock。使用unittest.mock,您可以在导入模块之前修补该功能。具体方法如下:

# test_annoying_file.py
import unittest.mock
from app import annoying_file

def test_normal_function():
    with unittest.mock.patch("app.annoying_file.annoying_function", return_value="foo"):
        from app.annoying_file import normal_function

        assert normal_function() == True

通过使用 unittest.mock.patch 作为上下文管理器,您可以在导入烦人的_文件之前修补烦人的_函数,确保它在导入过程中不会引发错误。

这种方法允许您隔离normal_function的单元测试,而无需修改原始代码或模拟烦人的_function的外部依赖项,使其更像是真正的单元测试。

© www.soinside.com 2019 - 2024. All rights reserved.