Утверждение последовательных вызовов фиктивного метода

175

У насмешки есть полезный assert_called_with()метод . Однако, насколько я понимаю, это проверяет только последний вызов метода.
Если у меня есть код, который вызывает проверяемый метод 3 раза подряд, каждый раз с разными параметрами, как я могу утверждать эти 3 вызова с их конкретными параметрами?

Джонатан
источник

Ответы:

179

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

Pigueiras
источник
9
Немного странно, что они решили добавить новый тип «вызова», для которого они могли бы также просто использовать список или кортеж ...
jaapz
@jaapz Это подклассы tuple: isinstance(mock.call(1), tuple)дает True. Они также добавили некоторые методы и атрибуты.
jpmc26
13
Ранние версии Mock использовали простой кортеж, но он оказался неудобным в использовании. Каждый вызов функции получает кортеж (args, kwargs), поэтому для проверки правильности вызова «foo (123)» необходимо «assert mock.call_args == ((123,), {})», что глоток по сравнению с «вызов (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
источник
@ jpmc26 не могли бы вы подробнее рассказать о ваших изменениях? Что вы имеете в виду под «лучше всего оставить без внимания»? Как еще вы будете проверять, был ли сделан вызов внутри метода
otgw
@memo Часто лучше позволить вызывать реальный метод. Если другой метод не работает, он может нарушить тест, но значение избегания меньше, чем значение более простого и более удобного теста. Лучшее время для подражания - когда внешний вызов другого метода является тем, что вы хотите протестировать (обычно это означает, что в него передается какой-то результат, а тестируемый код не возвращает результат.) Или другой метод. имеет внешние зависимости (базы данных, веб-сайты), которые вы хотите устранить. (Технически, последний случай - скорее заглушка, и я бы не стал на это утверждать.)
jpmc26
@ jpmc26 mocking полезен, когда вы хотите избежать внедрения зависимостей или какого-либо другого метода выбора стратегии времени выполнения. Как вы упомянули, тестирование внутренней логики методов без вызова внешних служб и, что более важно, без знания среды (нет, нет хорошего кода do() if TEST_ENV=='prod' else dont()), легко достигается путем насмешки, как вы предлагали. побочным эффектом этого является поддержание тестов по версиям (скажем, изменения кода между поиском Google api v1 и v2, ваш код будет тестировать версию 1, несмотря ни на что)
Даниэль Дубовски,
@DanielDubovski Большая часть вашего тестирования должна быть основана на вводе / выводе. Это не всегда возможно, но если это невозможно в большинстве случаев, возможно, у вас проблемы с дизайном. Когда вам нужно возвращаемое значение, которое обычно исходит из другого фрагмента кода, и вы хотите вырезать зависимость, обычно подойдет заглушка. Mocks необходимы только тогда, когда вам нужно убедиться, что вызывается какая-то функция, изменяющая состояние (возможно, без возвращаемого значения). (Разница между макетом и заглушкой в ​​том, что вы не утверждаете при вызове с заглушкой.) Использование заглушек там, где работают заглушки, делает ваши тесты менее удобными в обслуживании.
jpmc26
@ jpmc26 не называет внешний сервис своего рода выходом? Конечно, вы можете реорганизовать код, который создает сообщение для отправки, и протестировать его вместо утверждения параметров вызова, но ИМХО, это почти то же самое. Как бы вы предложили изменить дизайн вызова внешнего API? Я согласен, что насмешки должны быть сокращены до минимума, все, что я говорю, это то, что вы не можете проверять данные, которые вы отправляете во внешние сервисы, чтобы убедиться, что логика работает так, как ожидалось.
Даниэль Дубовски
47

Вы можете использовать Mock.call_args_listатрибут для сравнения параметров с предыдущими вызовами методов. Это в сочетании с Mock.call_countатрибутом должно дать вам полный контроль.

Джонатан
источник
9
assert_has_calls ()?
Баваза
5
assert_has_callsпроверяет только, были ли выполнены ожидаемые вызовы, но не проверяет, являются ли они единственными.
голубоглазый
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_callsна mock_work_function, а не на mock_work_function.return_value.

Педро М Дуарте
источник