【问题标题】:Asserting successive calls to a mock method断言对模拟方法的连续调用
【发布时间】:2011-11-06 17:33:36
【问题描述】:

Mock 有一个helpful assert_called_with() method。但是,据我了解,这只检查 last 对方法的调用。
如果我的代码连续调用了 3 次模拟方法,每次都使用不同的参数,我如何使用它们的特定参数断言这 3 次调用?

【问题讨论】:

    标签: python mocking


    【解决方案1】:

    assert_has_calls 是解决此问题的另一种方法。

    来自文档:

    assert_has_calls (调用,any_order=False)

    断言模拟已被 用指定的调用调用。检查 mock_calls 列表 来电。

    如果 any_order 为 False(默认值),则调用必须是顺序的。 在指定调用之前或之后可以有额外调用。

    如果 any_order 为 True,则调用可以按任何顺序进行,但它们必须 都出现在 mock_calls 中。

    例子:

    >>> from unittest.mock import call, Mock
    >>> mock = Mock(return_value=None)
    >>> mock(1)
    >>> mock(2)
    >>> mock(3)
    >>> mock(4)
    >>> calls = [call(2), call(3)]
    >>> mock.assert_has_calls(calls)
    >>> calls = [call(4), call(2), call(3)]
    >>> mock.assert_has_calls(calls, any_order=True)
    

    来源:https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls

    【讨论】:

    • 有点奇怪,他们选择添加一个新的“调用”类型,他们也可以使用列表或元组......
    • @jaapz 它是tuple 的子类:isinstance(mock.call(1), tuple) 给出True。他们还添加了一些方法和属性。
    • 早期版本的 Mock 使用纯元组,但使用起来很尴尬。每个函数调用都会收到一个 (args, kwargs) 的元组,因此要检查“foo(123)”是否被正确调用,需要“assert mock.call_args == ((123,), {})”,即与“call(123)”相比是一口
    • 当在每个调用实例中你期望不同的返回值时你会怎么做?
    • @CodeWithPride 它看起来更像是side_effect 的工作
    【解决方案2】:

    通常,我不关心调用的顺序,只关心它们发生了。在这种情况下,我将assert_any_call 与关于call_count 的断言结合起来。

    >>> import mock
    >>> m = mock.Mock()
    >>> m(1)
    <Mock name='mock()' id='37578160'>
    >>> m(2)
    <Mock name='mock()' id='37578160'>
    >>> m(3)
    <Mock name='mock()' id='37578160'>
    >>> m.assert_any_call(1)
    >>> m.assert_any_call(2)
    >>> m.assert_any_call(3)
    >>> assert 3 == m.call_count
    >>> m.assert_any_call(4)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "[python path]\lib\site-packages\mock.py", line 891, in assert_any_call
        '%s call not found' % expected_string
    AssertionError: mock(4) call not found
    

    我发现这样做比传递给单个方法的大量调用更容易阅读和理解。

    如果您确实关心订单或希望有多个相同的电话,assert_has_calls 可能更合适。

    编辑

    自从我发布此答案以来,我已经重新考虑了我的总体测试方法。我认为值得一提的是,如果您的测试变得如此复杂,那么您可能测试不当或存在设计问题。模拟设计用于测试面向对象设计中的对象间通信。如果您的设计不是面向对象的(如更多程序或功能),则模拟可能完全不合适。您可能在方法内部也有太多事情要做,或者您可能正在测试最好不要模拟的内部细节。当我的代码不是非常面向对象时,我开发了这种方法中提到的策略,并且我相信我也在测试最好不要模拟的内部细节。

    【讨论】:

    • @jpmc26 你能详细说明你的编辑吗?你所说的“最好的不被嘲笑”是什么意思?如果在方法中进行了调用,您将如何测试
    • @memo 通常,最好让真正的方法被调用。如果另一种方法被破坏,它可能会破坏测试,但避免它的价值小于拥有更简单、更可维护的测试的价值。模拟的最佳时间是当对另一个方法的外部调用是您要测试的(通常,这意味着某种结果被传递给它并且被测代码不返回结果。)或其他方法具有您想要消除的外部依赖项(数据库、网站)。 (从技术上讲,最后一个案例更像是一个存根,我会犹豫断言它。)
    • @jpmc26 模拟在您想避免依赖注入或其他一些运行时策略选择方法时很有用。就像您提到的那样,测试方法的内部逻辑,无需调用外部服务,更重要的是,无需了解环境(对于具有do() if TEST_ENV=='prod' else dont() 的好代码来说,不可以),可以通过模拟您建议的方式轻松实现。这样做的副作用是维护每个版本的测试(比如谷歌搜索 api v1 和 v2 之间的代码更改,无论如何你的代码都将测试版本 1)
    • @DanielDubovski 您的大部分测试都应该基于输入/输出。这并不总是可能的,但如果大多数时候都不可能,那么您可能遇到了设计问题。当您需要返回一些通常来自另一段代码的值并且您想要削减依赖项时,通常可以使用存根。仅当您需要验证是否调用了某些状态修改函数(可能没有返回值)时,才需要模拟。 (mock 和 stub 之间的区别在于,您不会在带有 stub 的调用中断言。)在 stub 可以使用的地方使用 mock 会降低您的测试的可维护性。
    • @jpmc26 调用外部服务不是一种输出吗?当然,您可以重构构建要发送的消息的代码并对其进行测试,而不是断言调用参数,但恕我直言,它几乎相同。您如何建议重新设计调用外部 API?我同意应该将模拟减少到最低限度,我只是说您不能四处测试发送到外部服务的数据以确保逻辑按预期运行。跨度>
    【解决方案3】:

    您可以使用Mock.call_args_list attribute 将参数与之前的方法调用进行比较。与Mock.call_count attribute 结合使用应该可以让您完全控制。

    【讨论】:

    • assert_has_calls 仅检查是否已完成预期的调用,但如果是唯一的调用则不检查。
    【解决方案4】:

    我总是要一次又一次地查看这个,所以这是我的答案。


    在同一类的不同对象上断言多个方法调用

    假设我们有一个重型类(我们想要模拟):

    In [1]: class HeavyDuty(object):
       ...:     def __init__(self):
       ...:         import time
       ...:         time.sleep(2)  # <- Spends a lot of time here
       ...:     
       ...:     def do_work(self, arg1, arg2):
       ...:         print("Called with %r and %r" % (arg1, arg2))
       ...:  
    

    下面是一些使用HeavyDuty 类的两个实例的代码:

    In [2]: def heavy_work():
       ...:     hd1 = HeavyDuty()
       ...:     hd1.do_work(13, 17)
       ...:     hd2 = HeavyDuty()
       ...:     hd2.do_work(23, 29)
       ...:    
    


    现在,这是heavy_work 函数的测试用例:

    In [3]: from unittest.mock import patch, call
       ...: def test_heavy_work():
       ...:     expected_calls = [call.do_work(13, 17),call.do_work(23, 29)]
       ...:     
       ...:     with patch('__main__.HeavyDuty') as MockHeavyDuty:
       ...:         heavy_work()
       ...:         MockHeavyDuty.return_value.assert_has_calls(expected_calls)
       ...:  
    

    我们正在用MockHeavyDuty 模拟HeavyDuty 类。要断言来自每个HeavyDuty 实例的方法调用,我们必须引用MockHeavyDuty.return_value.assert_has_calls,而不是MockHeavyDuty.assert_has_calls。此外,在expected_calls 列表中,我们必须指定我们对断言调用感兴趣的方法名称。所以我们的列表是由对call.do_work 的调用组成的,而不是简单的call

    执行测试用例表明它是成功的:

    In [4]: print(test_heavy_work())
    None
    


    如果我们修改 heavy_work 函数,测试将失败并产生有用的错误消息:

    In [5]: def heavy_work():
       ...:     hd1 = HeavyDuty()
       ...:     hd1.do_work(113, 117)  # <- call args are different
       ...:     hd2 = HeavyDuty()
       ...:     hd2.do_work(123, 129)  # <- call args are different
       ...:     
    
    In [6]: print(test_heavy_work())
    ---------------------------------------------------------------------------
    (traceback omitted for clarity)
    
    AssertionError: Calls not found.
    Expected: [call.do_work(13, 17), call.do_work(23, 29)]
    Actual: [call.do_work(113, 117), call.do_work(123, 129)]
    


    断言对函数的多次调用

    与上面的对比,这里有一个例子,展示了如何模拟对一个函数的多次调用:

    In [7]: def work_function(arg1, arg2):
       ...:     print("Called with args %r and %r" % (arg1, arg2))
    
    In [8]: from unittest.mock import patch, call
       ...: def test_work_function():
       ...:     expected_calls = [call(13, 17), call(23, 29)]    
       ...:     with patch('__main__.work_function') as mock_work_function:
       ...:         work_function(13, 17)
       ...:         work_function(23, 29)
       ...:         mock_work_function.assert_has_calls(expected_calls)
       ...:    
    
    In [9]: print(test_work_function())
    None
    


    有两个主要区别。第一个是在模拟一个函数时,我们使用call 设置我们预期的调用,而不是使用call.some_method。第二个是我们在mock_work_function 上调用assert_has_calls,而不是mock_work_function.return_value

    【讨论】:

    • 只是想让你知道这对我有很大帮助。谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-03-08
    • 1970-01-01
    • 2011-10-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多