web-dev-qa-db-ja.com

モックメソッドの連続呼び出しをアサートする

モックには 役に立つassert_called_with()メソッド があります。ただし、私が理解している限り、これはメソッドのlast呼び出しのみをチェックします。

130
Jonathan

Mock.call_args_list属性 を使用して、パラメーターを以前のメソッド呼び出しと比較できます。 Mock.call_count属性 と組み合わせることで、完全な制御が可能になります。

38
Jonathan

assert_has_callsは、この問題に対する別のアプローチです。

ドキュメントから:

assert_has_calls(calls、any_order = False)

指定された呼び出しでモックが呼び出されたことをアサートします。 mock_callsリストの呼び出しが確認されます。

Any_orderがFalse(デフォルト)の場合、呼び出しはシーケンシャルでなければなりません。指定された呼び出しの前後に余分な呼び出しがある場合があります。

Any_orderがTrueの場合、呼び出しは任意の順序にすることができますが、それらはすべてmock_callsに出現する必要があります。

例:

>>> from 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.Mock.assert_has_calls

135
Pigueiras

通常、呼び出しの順序は気にしません。呼び出しが行われたということだけです。その場合、 assert_any_callcall_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

この方法で行うと、1つのメソッドに渡される呼び出しの大きなリストよりも読みやすく、理解しやすくなります。

順序を気にする場合、または複数の同一の呼び出しが予想される場合は、 assert_has_calls の方が適切です。

編集

この回答を投稿してから、一般的なテストへのアプローチを見直しました。テストがこれほど複雑になっている場合、テストが不適切であるか、設計に問題がある可能性があることに言及する価値があると思います。モックは、オブジェクト指向設計でオブジェクト間通信をテストするために設計されています。デザインがオブジェクト指向ではない場合(より手続き的または機能的に)、モックは完全に不適切である可能性があります。また、メソッド内であまりにも多くの処理が行われている場合や、モックされないままにしておくのが最適な内部の詳細をテストしている場合もあります。私のコードがあまりオブジェクト指向ではないときにこのメソッドで言及した戦略を開発しました。また、モックをかけずに残しておくとよい内部の詳細もテストしていたと思います。

85
jpmc26

私はいつもこれを何度も見直さなければならないので、ここに私の答えがあります。


同じクラスの異なるオブジェクトで複数のメソッド呼び出しをアサートする

ヘビーデューティークラス(モックしたい)があるとします。

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クラスの2つのインスタンスを使用するコードを次に示します。

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とは対照的に、call.do_workの呼び出しで構成されています。

テストケースを実行すると、成功したことがわかります。

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


主に2つの違いがあります。 1つ目は、関数をモックするときに、call.some_methodを使用する代わりに、callを使用して予想される呼び出しをセットアップすることです。 2つ目は、assert_has_callsではなく、mock_work_functionmock_work_function.return_valueを呼び出すことです。

16
Pedro M Duarte