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


175

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

Answers:


179

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


9
他们选择添加一个新的“调用”类型
有点奇怪

@jaapz它子类化tupleisinstance(mock.call(1), tuple)给出True。他们还添加了一些方法和属性。
2015年

13
早期的Mock版本使用普通的元组,但是事实证明使用起来很尴尬。每个函数调用都接收一个(args,kwargs)元组,因此要检查是否正确调用了“ foo(123)”,您需要“断言ockmock.call_args ==(((123,),{}))”,这是一口相比“ call(123)”
乔纳森·哈特利

当您在每个调用实例上期望不同的返回值时,该怎么办?
CodeWithPride

2
@CodeWithPride它看起来更多工作side_effect
Pigueiras

108

通常,我不关心呼叫的顺序,只关心它们的发生。在这种情况下,我结合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您能否详细说明您的编辑?“最好不要假装”是什么意思?您还将如何测试是否已在某个方法中进行了调用
otgw 2015年

@memo通常,最好调用real方法。如果其他方法被破坏,则可能会破坏测试,但是避免这种情况的价值小于进行更简单,更可维护的测试的价值。最好的模拟时间是当您要测试对另一个方法的外部调用时(通常,这意味着将某种结果传递给它,并且被测试的代码不返回结果。)或另一个方法有要消除的外部依赖项(数据库,网站)。(从技术上讲,最后一种情况更多是存根,我会对此断言。)
jpmc26 2015年

当您希望避免依赖项注入或其他一些运行时策略选择方法时,@ jpmc26模拟非常有用。就像您提到的那样,do() if TEST_ENV=='prod' else dont()通过模拟建议的方法可以轻松地实现方法内部逻辑的测试,而无需调用外部服务,更重要的是,无需了解环境(对良好的代码不反对)。这样做的副作用是维护每个版本的测试(例如,在Google Search api v1和v2之间更改代码,无论什么情况,您的代码都会测试版本1)
Daniel Dubovski

@DanielDubovski您的大多数测试应基于输入/输出。这并非总是可能的,但是如果在大多数情况下不可能,则可能存在设计问题。当您需要返回通常来自另一段代码的某些值并且想要削减依赖关系时,通常会使用存根。只有在需要验证是否调用了某些状态修改功能(可能没有返回值)时,才需要使用模拟。(模拟和存根之间的区别在于,您不必在使用存根的调用上进行断言。)在将存根用作对象的地方使用模拟会使测试的可维护性降低。
jpmc26

@ jpmc26不是将外部服务称为一种输出吗?当然,您可以重构用于构建要发送的消息的代码并对其进行测试,而无需声明调用参数,但是恕我直言,这几乎是相同的。我建议重新设计调用的外部API?我同意应将模拟工作减少到最低程度,我只是说您不能绕过发送给外部服务的数据,以确保逻辑行为符合预期。
丹尼尔·杜波夫斯基


17

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


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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.