问题 使用Python进行模拟时避免使用冗余@patch


来自静态编程语言背景,我想知道如何最好地在Python中进行模拟。我习惯于依赖注入。在测试中,模拟被创建并传递给被测系统(SUT)。但是,看看Mock和Python的其他模拟框架,它似乎是类型/函数/等。在逐个测试的基础上更换模块中的模块。

特别是,对于Mock,你会说每个单元测试 @patch('some.type.in.the.module.under.test') 对于每种类型/功能/等。你想嘲笑。在测试的一生中,这些东西被嘲笑,然后它们被还原。不幸的是,在整个测试过程中,灯具非常接近相同,你最终会重复你的 @patch一遍又一遍。

我想要一种在单元测试中共享补丁集合的方法。我还希望以可组合的方式对夹具进行调整。我可以使用上下文管理器而不是装饰器。


3541
2017-07-25 19:56


起源

嗯,我一直认为SUT的意思是“被测软件”,而不是“系统”。学到了新东西! - voithos


答案:


您可以修补流向该类中每个方法的测试类。然后你可以从超类继承并使用setUp和tearDown方法。

import unittest 

@patch('some.type.in.the.module.under.test')
class MySuperTestCase(unittest.TestCase):
    pass

class MyActualTestCase(MySuperTestCase):

    def test_method(self, mock_function)
        mock_function.return_value = False

这不像你想象的那么普遍。因为您需要在对象的确切位置对其进行修补。你不修补'sys.stdout',你补丁'my_dir.my_module.sys.stdout'。因此,在测试特定模块时,它才真正有用。但是为了测试那个特定的模型,你当然只需要一个补丁修饰器。


9
2017-07-26 08:07



我不知道 @patch 在课堂上工作。这是否意味着每个测试方法必须为每个修补对象都有一个参数? - Travis Parks
是的,他们都是位置的。对于你不关心它们的情况,总有* args - aychedee
这仍然很棒。在阅读文档时我甚至没有看到它。不过,这需要“每个夹具的测试类”,但它正朝着正确的方向发展。 - Travis Parks
我单元测试的方式我有许多测试类,它们执行不同类型的设置,然后至少有一个(如果不是更多)测试类继承每个测试模块的各种超类。适合我。修补模块通常对每个测试都是如此具体,我并没有真正感觉到很多重复。 - aychedee


我最近遇到了类似的情况但更极端。我的一个顶级模块必须模拟出几个存储库,提供程序和逻辑库。这导致了许多需要的单元测试 @patch 7个组件。我想避免大量重复的测试代码,所以这里是我的解决方案,效果很好:

@mock.patch('module.blah1.method1')      # index: 6
@mock.patch('module.blah1.method2')      # index: 5
@mock.patch('module.blah2.method1')      # index: 4
@mock.patch('module.blah2.method2')      # index: 3
@mock.patch('module.blah2.method3')      # index: 2
@mock.patch('module.blah3.method1')      # index: 1
@mock.patch('module.blah4.method1')      # index: 0
class TestsForMyCode(unittest.TestCase):

    def test_first_test(self, *mocks):
        # Arrange

        # setup mocks for only the ones that need specific mocked behaviours

        # idx 2 patches module.blah2.method3
        mocks[2].return_value = 'some value'

        # Act
        target = sut()
        result = target.do_something()

        # Assert
        assert result is False

    def test_second_test(self, *mocks):
        # Arrange

        # setup mocks for only the ones that need specific mocked behaviours

        # idx 0 patches module.blah4.method1
        mocks[0].return_value = 'another value'

        # idx 4 patches module.blah2.method1
        mocks[4].return_value = 'another value'

        # Act
        target = sut()
        result = target.do_something_else()

        # Assert
        assert result is True

@mock运行时将类上的s应用于每个测试,并将所有修补程序传递到* mocks参数中。要记住的重要事项是排序 - 我将索引注释放在我的代码中,以便将它直接放在我的脑海中。

希望这可以帮助。


3
2017-08-23 01:41



那太棒了,谢谢。 - Alex Harvey


我不保证这在语法上是正确的,因为我无法测试它,但是这里是:

COMMON_FUNCTIONS = ('some.type.in.the.module.under.test', 'and.others')
def common_patches(f):
    for item in COMMON_FUNCTIONS:
        f = patch(item)(f)

现在应用它:

@common_patches
def something():
    pass # it will be decorated by all the patches in the function

1
2017-07-25 20:04



像这样使用COMMON_FUNCTIONS是一种非常一劳永逸的方法。我需要使用共享夹具构建逻辑每天编写几十次单元测试。我不认为装饰者是解决方案。虽然,我认为基于上下文管理器的解决方案也是如此。 - Travis Parks


我也会推荐 装饰,因为你可以避免多余 补丁。而不只是,使用 参数化装饰器,您可以控制每个装饰器的自定义灯具。例:

def patch_example(custom_value=None):
    def _patch(test_func):
        @mock.patch('some.type.in.the.module.under.test')
        def _patch_it(mocked_function):
            mocked_function = custom_value
            return test_func(self)
        return wraps(test_func)(_patch_it)
    return _patch

class ExampleTestCase(object):

    @patch_example(custom_value='new_value')
    def test_method_1(self):
        # your test logic here, with mocked values already defined in decorator

    @patch_example(custom_value='new_value_2')
    def test_method_2(self):
        # your test logic here, with mocked values already defined in decorator

1
2017-10-06 09:47