Python:是否可以包装“@patch(path)”以供重用?(单元测试)

2024-09-30 20:35:37 发布

您现在位置:Python中文网/ 问答频道 /正文

正如doc"Where to patch"所说,我们需要修补查找对象的位置(而不是定义对象的位置);所以我明白,不可能——比方说——为特定路径创建可重用补丁

假设有几个模块导入一个要模拟的对象

# file_a.py
from foo.goo.hoo import settings
# file_b.py
from foo.goo.hoo import settings
# file_c.py
from foo.goo.hoo import settings

我想知道是否有一种方法可以创建一个装饰器,例如:

@mock_settings
def test_whatever(self, settings_mock):
    ...

而不是此解决方案:

@patch("some_module.file_a.settings")
def test_whatever(self, settings_mock):
    ...
@patch("some_module.file_b.settings")
def test_whatever(self, settings_mock):
    ...
@patch("some_module.file_c.settings")
def test_whatever(self, settings_mock):
    ...

Tags: 对象frompytestimportselfsettingsfoo
1条回答
网友
1楼 · 发布于 2024-09-30 20:35:37

如问题中所述,要修补对象,必须修补其在待测试模块中的引用(如果使用from ...import导入对象)。
要在多个模块中对其进行修补,可以使用相同的模拟对所有这些模块进行修补,并使用该模拟。如果您事先知道要修补哪些模块,您可以这样做。如果您事先不知道它们,您必须尝试在所有加载的模块中修补对象,这可能会变得更加复杂

我将展示一个使用pytest和pytest夹具的示例,因为这更紧凑;您可以将其包装在装饰器中,以便在unittest中使用,但这不会改变基本情况。考虑到我们有一个需要在几个模块中嘲笑的类:

class\u to\u mock.py

class ClassToMock:
    def foo(self, msg):
        return msg

module1.py

from class_to_mock import ClassToMock

def do_something():
    inst = ClassToMock()
    return inst.foo("module1")

module2.py

from class_to_mock import ClassToMock

def do_something_else():
    inst = ClassToMock()
    return inst.foo("module2")

现在,您可以编写一个fixture,一次模拟所有这些模块中的类(为了简单起见,这里使用pytest-mock):

@pytest.fixture
def mocked_class(mocker):
    mocked = Mock()
    for module in ('module1', 'module2'):
        mocker.patch(module + '.ClassToMock', mocked)
    yield mocked

这可用于测试两个模块:

def test_module1(mocked_class):
    mocked_class.return_value.foo.return_value = 'mocked!'
    assert module1.do_something() == 'mocked!'

def test_module2(mocked_class):
    mocked_class.return_value.foo.return_value = 'mocked!'
    assert module2.do_something_else() == 'mocked!'

如果您想要在所有加载的模块中模拟类的通用版本,您可以使用以下内容替换该夹具:

@pytest.fixture
def mocked_class(mocker):
    mocked = Mock()
    for name, module in list(sys.modules.items()):
        if not inspect.ismodule(module):
            continue
        for cls_name, cls in module.__dict__.items():
            try:  # need that as inspect may raise for some modules
                if inspect.isclass(cls) and cls_name == "ClassToMock":
                    mocker.patch(name + ".ClassToMock", mocked)
            except Exception:
                continue
    yield mocked

这将适用于这个特定的例子-为了概括这一点,它必须考虑更多的对象类型,类应该是可配置的,并且可能会有更多的问题-与更简单的版本相反,在这里你只需要枚举要修补的模块,这将一直有效。p>

unittest.setUp中,您可以通过将mock放在实例变量中来执行类似的操作,尽管这样做不太优雅,因为您还负责停止mock:

class ModulesTest(unittest.TestCase):
    def setUp(self):
        self.mocked_class = Mock()
        self.mocks = []
        for module in ('module1', 'module2'):
            mocked = mock.patch(module + '.ClassToMock', self.mocked_class)
            self.mocks.append(mocked)
            mocked.start()

    def tearDown(self):
        for mocked in self.mocks:
            mocked.stop()

    def test_module1(self):
        self.mocked_class.return_value.foo.return_value = 'mocked!'
        assert module1.do_something() == 'mocked!'

您也可以将其包装在装饰器中,以至少部分地回答您最初的问题:

def mocked_class_to_mock(f):
    @wraps(f)
    def _mocked_class_to_mock(*args, **kwargs):
        mocked_class = Mock()
        mocks = []
        for module in ('module1', 'module2'):
            mocked = mock.patch(module + '.ClassToMock', mocked_class)
            mocks.append(mocked)
            mocked.start()
        kwargs['mocked_class'] = mocked_class  # use a keyword arg for simplicity
        f(*args, **kwargs)
        for mocked in mocks:
            mocked.stop()

    return _mocked_class_to_mock

...
    @mocked_class_to_mock
    def test_module3(self, mocked_class):
        mocked_class.return_value.foo.return_value = 'mocked!'
        assert module3.do_something() == 'mocked!'

当然,如果需要,您也可以对更通用的版本执行相同的操作

还请注意,我跳过了使用import ...导入对象的简单情况。在这种情况下,您必须修补原始模块。在通用夹具中,您可能希望始终添加这种情况

相关问题 更多 >