【发布时间】:2011-11-06 17:33:36
【问题描述】:
Mock 有一个helpful assert_called_with() method。但是,据我了解,这只检查 last 对方法的调用。
如果我的代码连续调用了 3 次模拟方法,每次都使用不同的参数,我如何使用它们的特定参数断言这 3 次调用?
【问题讨论】:
Mock 有一个helpful assert_called_with() method。但是,据我了解,这只检查 last 对方法的调用。
如果我的代码连续调用了 3 次模拟方法,每次都使用不同的参数,我如何使用它们的特定参数断言这 3 次调用?
【问题讨论】:
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
【讨论】:
tuple 的子类:isinstance(mock.call(1), tuple) 给出True。他们还添加了一些方法和属性。
side_effect 的工作
通常,我不关心调用的顺序,只关心它们发生了。在这种情况下,我将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 可能更合适。
自从我发布此答案以来,我已经重新考虑了我的总体测试方法。我认为值得一提的是,如果您的测试变得如此复杂,那么您可能测试不当或存在设计问题。模拟设计用于测试面向对象设计中的对象间通信。如果您的设计不是面向对象的(如更多程序或功能),则模拟可能完全不合适。您可能在方法内部也有太多事情要做,或者您可能正在测试最好不要模拟的内部细节。当我的代码不是非常面向对象时,我开发了这种方法中提到的策略,并且我相信我也在测试最好不要模拟的内部细节。
【讨论】:
do() if TEST_ENV=='prod' else dont() 的好代码来说,不可以),可以通过模拟您建议的方式轻松实现。这样做的副作用是维护每个版本的测试(比如谷歌搜索 api v1 和 v2 之间的代码更改,无论如何你的代码都将测试版本 1)
您可以使用Mock.call_args_list attribute 将参数与之前的方法调用进行比较。与Mock.call_count attribute 结合使用应该可以让您完全控制。
【讨论】:
assert_has_calls 仅检查是否已完成预期的调用,但如果是唯一的调用则不检查。
我总是要一次又一次地查看这个,所以这是我的答案。
假设我们有一个重型类(我们想要模拟):
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。
【讨论】: