У Mock есть полезный assert_called_with()
. Однако, насколько я понимаю, это проверяет только последний вызов метода.
Если у меня есть код, который вызывает проверяемый метод 3 раза подряд, каждый раз с разными параметрами, как я могу утверждать эти 3 вызова с их конкретными параметрами?
Утверждение последовательных вызовов макету
Ответ 1
Вы можете использовать атрибут Mock.call_args_list
для сравнения параметров с предыдущими вызовами методов. Это в сочетании с атрибутом Mock.call_count
должно дать вам полный контроль.
Ответ 2
assert_has_calls
- еще один подход к этой проблеме.
Из документов:
assert_has_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.assert_has_calls
Ответ 3
Обычно меня не волнует порядок звонков, только то, что они произошли. В этом случае я объединяю 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
может быть более подходящим.
редактировать
Поскольку я опубликовал этот ответ, я переосмыслил свой подход к тестированию в целом. Я думаю, стоит упомянуть, что если ваш тест усложняется, возможно, вы тестируете не по назначению или у вас есть проблемы с дизайном. Макеты предназначены для тестирования межобъектной коммуникации в объектно-ориентированном дизайне. Если ваш дизайн не ориентирован на возражения (как в более процедурном или функциональном), макет может быть совершенно неуместным. Возможно, внутри метода слишком много всего происходит, или вы можете тестировать внутренние детали, которые лучше не менять. Я разработал стратегию, упомянутую в этом методе, когда мой код был не очень объектно-ориентированным, и я считаю, что я также тестировал внутренние детали, которые лучше было бы оставить без проверки.
Ответ 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)
...:
Мы издеваемся над классом 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_calls
на mock_work_function
, а не на mock_work_function.return_value
.