Затвердження послідовних викликів до макетного методу


175

Макет має корисний assert_called_with()метод . Однак, наскільки я розумію, це перевіряє лише останній виклик методу.
Якщо у мене є код, який викликає глузливий метод 3 рази поспіль, кожен раз з різними параметрами, то як я можу підтвердити ці 3 виклики з їх конкретними параметрами?

Відповіді:


179

assert_has_calls є ще один підхід до цієї проблеми.

З документів:

assert_has_calls (дзвінки, any_order = помилково)

стверджувати, що макет викликався з вказаними дзвінками. Список mock_calls перевіряється на дзвінки.

Якщо any_order є False (за замовчуванням), то виклики повинні бути послідовними. Можуть бути додаткові дзвінки до або після вказаних дзвінків.

Якщо any_order істинний, то дзвінки можуть бути в будь-якому порядку, але всі вони повинні відображатися в 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


9
Трохи дивно вони вирішили додати новий тип "виклику", для якого вони також могли просто використати список або кортеж ...
jaapz

@jaapz Це підкласи tuple: isinstance(mock.call(1), tuple)дає True. Вони також додали деякі методи та ознаки.
jpmc26

13
Ранні версії Mock використовували звичайний кортеж, але виявляється незручним у використанні. Кожен виклик функції отримує кортеж (args, kwargs), тому для перевірки правильності виклику "foo (123)" вам потрібно "затвердити 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 Ви могли б детальніше розробити свої зміни? Що ви маєте на увазі під «найкращим лівим незамкненим»? Як ще ви перевірите, чи було здійснено дзвінок у межах методу
otgw

@memo Найчастіше краще дозволити називати справжній метод. Якщо інший метод порушений, він може порушити тест, але значення уникнення його менше, ніж значення простішого, більш ремонтованого тесту. Найкращі часи для знущань - коли зовнішній виклик іншого методу - це те, що ви хочете перевірити (як правило, це означає, що в нього передається якийсь результат, і тестовий код не повертає результат.) Або інший метод має зовнішні залежності (база даних, веб-сайти), які ви хочете усунути. (Технічно останній випадок - це більше заглушка, і я б вагався стверджувати про це.)
jpmc26

@ jpmc26 глузування корисно, коли ви хочете уникнути введення залежності або іншого вибору стратегії виконання часу. як ви вже згадували, тестування внутрішньої логіки методів, без виклику зовнішніх служб, і що ще важливіше, без усвідомлення оточуючих (ну, ні, для того, щоб мати хороший код do() if TEST_ENV=='prod' else dont()), досягається легко, глузуючи з запропонованого вами способу. побічним ефектом цього є підтримка тестів для версій (скажімо, зміни коду між google search api v1 та v2, ваш код перевірить версію 1, незважаючи ні на що)
Даніель Дубовський

@DanielDubovski Більшість ваших тестувань має ґрунтуватися на вході / виході. Це не завжди можливо, але якщо це неможливо більшу частину часу, у вас, ймовірно, є проблеми із дизайном. Коли вам потрібне якесь повернене значення, яке зазвичай надходить з іншого фрагмента коду, і ви хочете вирізати залежність, зазвичай це зробить заглушка. Знущання необхідні лише тоді, коли вам потрібно переконатися, що якась функція зміни стану (можливо, без значення повернення) викликається. (Різниця між макетом та заглушкою полягає в тому, що ви не заявляєте про дзвінок із заглушкою.) Використання макетів, в яких виконуватимуться заглушки, робить ваші тести менш вигідними.
jpmc26

@ jpmc26 не називає зовнішню службу видом результату? звичайно, ви можете перефактурувати код, який створює повідомлення, яке слід відправити, і протестувати його замість того, щоб стверджувати парами викликів, але IMHO, це майже все те саме. Як би ви запропонували переробити дизайн зовнішніх API?
Даніель Дубовський

46

Ви можете використовувати 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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.