断言对模拟方法的后续调用

问题:断言对模拟方法的后续调用

模拟有一个有用的assert_called_with()方法。但是,据我了解,这仅检查对方法的最后一次调用。
如果我有连续3次调用该模拟方法的代码,每次使用不同的参数,那么该如何用其特定的参数来断言这3次调用?

Mock has a helpful assert_called_with() method. However, as far as I understand this only checks the last call to a method.
If I have code that calls the mocked method 3 times successively, each time with different parameters, how can I assert these 3 calls with their specific parameters?


回答 0

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

从文档:

assert_has_calls (calls,any_order = False)

断言已使用指定的调用调用了该模拟。检查嘲笑列表中是否有呼叫。

如果any_order为False(默认设置),则调用必须是连续的。在指定呼叫之前或之后可能会有额外的呼叫。

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

例:

>>> 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

assert_has_calls is another approach to this problem.

From the docs:

assert_has_calls (calls, any_order=False)

assert the mock has been called with the specified calls. The mock_calls list is checked for the calls.

If any_order is False (the default) then the calls must be sequential. There can be extra calls before or after the specified calls.

If any_order is True then the calls can be in any order, but they must all appear in mock_calls.

Example:

>>> 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)

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


回答 1

通常,我不关心呼叫的顺序,只关心它们的发生。在这种情况下,我结合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可能更合适。

编辑

自从我发布了这个答案以来,我就重新考虑了一般的测试方法。我认为值得一提的是,如果您的测试变得如此复杂,则可能是测试不合适或存在设计问题。模拟设计用于在面向对象的设计中测试对象之间的通信。如果您的设计不是面向对象的(如在更多过程或功能上),则该模拟可能完全不合适。您可能还会在方法内部进行过多操作,或者您可能正在测试最好不要进行内部模拟的内部细节。当我的代码不是非常面向对象时,我开发了此方法中提到的策略,并且我相信我也在测试内部细节,而这些细节最好不要假装。

Usually, I don’t care about the order of the calls, only that they happened. In that case, I combine assert_any_call with an assertion about 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

I find doing it this way to be easier to read and understand than a large list of calls passed into a single method.

If you do care about order or you expect multiple identical calls, assert_has_calls might be more appropriate.

Edit

Since I posted this answer, I’ve rethought my approach to testing in general. I think it’s worth mentioning that if your test is getting this complicated, you may be testing inappropriately or have a design problem. Mocks are designed for testing inter-object communication in an object oriented design. If your design is not objected oriented (as in more procedural or functional), the mock may be totally inappropriate. You may also have too much going on inside the method, or you might be testing internal details that are best left unmocked. I developed the strategy mentioned in this method when my code was not very object oriented, and I believe I was also testing internal details that would have been best left unmocked.


回答 2

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

You can use the Mock.call_args_list attribute to compare parameters to previous method calls. That in conjunction with Mock.call_count attribute should give you full control.


回答 3

我总是不得不一次又一次地看一下这个,所以这是我的答案。


在同一个类的不同对象上声明多个方法调用

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

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)
   ...:  

我们正在用嘲笑HeavyDuty课堂MockHeavyDuty。要断言来自每个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。第二个就是我们所说assert_has_callsmock_work_function,而不是上mock_work_function.return_value

I always have to look this one up time and time again, so here is my answer.


Asserting multiple method calls on different objects of the same class

Suppose we have a heavy duty class (which we want to mock):

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))
   ...:  

here is some code that uses two instances of the HeavyDuty class:

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


Now, here is a test case for the heavy_work function:

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)
   ...:  

We are mocking the HeavyDuty class with MockHeavyDuty. To assert method calls coming from every HeavyDuty instance we have to refer to MockHeavyDuty.return_value.assert_has_calls, instead of MockHeavyDuty.assert_has_calls. In addition, in the list of expected_calls we have to specify which method name we are interested in asserting calls for. So our list is made of calls to call.do_work, as opposed to simply call.

Exercising the test case shows us it is successful:

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


If we modify the heavy_work function, the test fails and produces a helpful error message:

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)]


Asserting multiple calls to a function

To contrast with the above, here is an example that shows how to mock multiple calls to a function:

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


There are two main differences. The first one is that when mocking a function we setup our expected calls using call, instead of using call.some_method. The second one is that we call assert_has_calls on mock_work_function, instead of on mock_work_function.return_value.