Potwierdzanie kolejnych wywołań metody pozorowanej

175

Mock ma pomocną assert_called_with()metodę . Jednak, o ile rozumiem, sprawdza to tylko ostatnie wywołanie metody.
Jeśli mam kod, który wywołuje symulowaną metodę 3 razy z rzędu, za każdym razem z innymi parametrami, w jaki sposób mogę potwierdzić te 3 wywołania z ich określonymi parametrami?

Jonathan
źródło

Odpowiedzi:

179

assert_has_calls to inne podejście do tego problemu.

Z dokumentów:

assert_has_calls (połączenia, any_order = False)

potwierdzić, że mock został wywołany z określonymi wywołaniami. Lista mock_calls jest sprawdzana pod kątem połączeń.

Jeśli any_order ma wartość False (wartość domyślna), wywołania muszą być sekwencyjne. Mogą być dodatkowe połączenia przed lub po określonych połączeniach.

Jeśli any_order ma wartość True, wtedy wywołania mogą być w dowolnej kolejności, ale wszystkie muszą pojawić się w mock_calls.

Przykład:

>>> 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)

Źródło: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls

Pigueiras
źródło
9
Trochę dziwne, że zdecydowali się dodać nowy typ „połączenia”, do którego mogli również użyć listy lub krotki ...
jaapz
@jaapz To podklasy tuple: isinstance(mock.call(1), tuple)daje True. Dodali także kilka metod i atrybutów.
jpmc26
13
Wczesne wersje Mocka wykorzystywały zwykłą krotkę, ale okazuje się, że jest niewygodna w użyciu. Każde wywołanie funkcji otrzymuje krotkę (args, kwargs), więc aby sprawdzić, czy „foo (123)” zostało poprawnie wywołane, musisz „assert mock.call_args == ((123,), {})”, czyli kęs w porównaniu do „rozmowy (123)”
Jonathan Hartley
Co robisz, gdy przy każdym wywołaniu oczekujesz innej wartości zwracanej?
CodeWithPride
2
@CodeWithPride wygląda bardziej na pracęside_effect
Pigueiras
108

Zwykle nie obchodzi mnie kolejność połączeń, tylko to, że się wydarzyły. W takim przypadku łączę się assert_any_callz twierdzeniem o 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

Uważam, że zrobienie tego w ten sposób jest łatwiejsze do odczytania i zrozumienia niż duża lista wywołań przekazywana do jednej metody.

Jeśli zależy Ci na porządku lub spodziewasz się wielu identycznych połączeń, assert_has_callsmoże być bardziej odpowiednie.

Edytować

Odkąd opublikowałem tę odpowiedź, przemyślałem ogólnie swoje podejście do testowania. Myślę, że warto wspomnieć, że jeśli twój test staje się tak skomplikowany, możesz testować niewłaściwie lub masz problem z projektem. Makiety są przeznaczone do testowania komunikacji między obiektami w projekcie zorientowanym obiektowo. Jeśli Twój projekt nie jest zorientowany na zastrzeżenia (np. Bardziej proceduralny lub funkcjonalny), próba może być całkowicie nieodpowiednia. Być może w metodzie dzieje się zbyt wiele lub możesz testować wewnętrzne szczegóły, które najlepiej pozostawić niezablokowane. Opracowałem strategię wspomnianą w tej metodzie, gdy mój kod nie był zbyt zorientowany obiektowo i uważam, że testowałem również wewnętrzne szczegóły, które najlepiej byłoby pozostawić niezamokowane.

jpmc26
źródło
@ jpmc26 czy mógłbyś bardziej szczegółowo opisać swoją edycję? Co masz na myśli, mówiąc „najlepiej pozostawić niezablokowany”? Jak inaczej mógłbyś sprawdzić, czy wywołanie zostało wykonane w ramach metody
otgw,
@memo Często lepiej jest wywołać prawdziwą metodę. Jeśli druga metoda jest zepsuta, może to spowodować przerwanie testu, ale wartość unikania tego jest mniejsza niż wartość posiadania prostszego, łatwiejszego do utrzymania testu. Najlepszym momentem do mockowania jest to, że zewnętrzne wywołanie innej metody jest tym, co chcesz przetestować (zwykle oznacza to, że jakiś wynik jest do niego przekazywany, a testowany kod nie zwraca wyniku) lub inna metoda. ma zewnętrzne zależności (baza danych, strony internetowe), które chcesz wyeliminować. (Technicznie rzecz biorąc, ostatni przypadek jest raczej
niedopałkiem
Mockowanie @ jpmc26 jest przydatne, gdy chcesz uniknąć wstrzykiwania zależności lub innej metody wyboru strategii środowiska wykonawczego. jak wspomniałeś, testowanie wewnętrznej logiki metod bez wywoływania usług zewnętrznych, a co ważniejsze, bez bycia świadomym środowiska (nie, nie dla dobrego kodu do() if TEST_ENV=='prod' else dont()), można łatwo osiągnąć przez kpienie z zasugerowanego sposobu. efektem ubocznym jest utrzymanie testów na wersje (powiedzmy, że zmiany kodu między API wyszukiwarki Google v1 i v2, Twój kod będzie testował wersję 1 bez względu na wszystko)
Daniel Dubovski
@DanielDubovski Większość testów powinna być oparta na danych wejściowych / wyjściowych. Nie zawsze jest to możliwe, ale jeśli nie jest to możliwe przez większość czasu, prawdopodobnie masz problem z projektem. Kiedy potrzebujesz zwróconej wartości, która zwykle pochodzi z innego fragmentu kodu i chcesz usunąć zależność, zwykle wystarczy stub. Mocks są konieczne tylko wtedy, gdy trzeba sprawdzić, czy wywoływana jest jakaś funkcja modyfikująca stan (prawdopodobnie bez wartości zwracanej). (Różnica między mock a stubem polega na tym, że nie potwierdzasz wywołania z stubem). Używanie mocków tam, gdzie robią to stuby, sprawia, że ​​testy są trudniejsze do utrzymania.
jpmc26
@ jpmc26 czy wywołanie usługi zewnętrznej nie jest rodzajem wyjścia? oczywiście możesz zrefaktoryzować kod, który tworzy wiadomość do wysłania i przetestować go zamiast potwierdzać parametry wywołania, ale IMHO, to prawie to samo. Jak sugerowałbyś przeprojektowanie wywoływania zewnętrznych interfejsów API? Zgadzam się, że mockowanie powinno być ograniczone do minimum, mówię tylko, że nie możesz testować danych wysyłanych do usług zewnętrznych, aby upewnić się, że logika działa zgodnie z oczekiwaniami.
Daniel Dubovski
46

Możesz użyć Mock.call_args_listatrybutu, aby porównać parametry z poprzednimi wywołaniami metod. To w połączeniu z Mock.call_countatrybutem powinno dać Ci pełną kontrolę.

Jonathan
źródło
9
assert_has_calls ()?
bavaza
5
assert_has_callssprawdza tylko, czy oczekiwane wywołania zostały wykonane, ale nie jeśli są jedynymi.
niebieskawy
17

Zawsze muszę to raz sprawdzać, więc oto moja odpowiedź.


Asserting wielu wywołań metod na różnych obiektach tej samej klasy

Załóżmy, że mamy klasę heavy duty (którą chcemy kpić):

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))
   ...:  

tutaj jest kod, który używa dwóch instancji HeavyDutyklasy:

In [2]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(13, 17)
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(23, 29)
   ...:    


Oto przypadek testowy dla heavy_workfunkcji:

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)
   ...:  

Kpimy z HeavyDutyklasy MockHeavyDuty. Aby potwierdzić wywołania metod pochodzące z każdej HeavyDutyinstancji, musimy odwoływać się MockHeavyDuty.return_value.assert_has_callszamiast MockHeavyDuty.assert_has_calls. Ponadto na liście expected_callsmusimy określić, dla której nazwy metody jesteśmy zainteresowani potwierdzaniem wywołań. Tak więc nasza lista składa się z połączeń do call.do_work, a nie po prostu call.

Ćwiczenie przypadku testowego pokazuje nam, że się udało:

In [4]: print(test_heavy_work())
None


Jeśli zmodyfikujemy heavy_workfunkcję, test zakończy się niepowodzeniem i wyświetli pomocny komunikat o błędzie:

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)]


Potwierdzanie wielu wywołań funkcji

W przeciwieństwie do powyższego, oto przykład, który pokazuje, jak mockować wiele wywołań funkcji:

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


Istnieją dwie główne różnice. Pierwszą z nich jest to, że podczas mockowania funkcji konfigurujemy nasze oczekiwane wywołania za pomocą callzamiast używać call.some_method. Drugim jest to, że możemy zadzwonić assert_has_callsna mock_work_function, zamiast na mock_work_function.return_value.

Pedro M. Duarte
źródło