-
Notifications
You must be signed in to change notification settings - Fork 0
TECH TALK 3 MOCKOWANIE
from mock import Mock
m = Mock()Mock to sztuczny obiekt naśladujący/zastępujący obiekt prawdziwy, który wykorzystujemy w testach gdyż użycie prawdziwego obiektu byłoby zbyt skomplikowane, kosztowne (np. obliczeniowo), itp. Mocka w Pythonie możemy programować, tj. określać jego zachowanie. Są na to dwa główne sposoby:
-
return_value: określamy wartość jaka będzie zwracana przy wywołanu mocka -
side_effect: określamy "efekt uboczny" wywołania mocka:
- w przypadku iterabli, kolejne wywołania będą zwracać kolejne wartośći iterabla, a gdy wartości "wyczerpią się", zostanie rzucony wyjątek
StopIteration:
m = Mock()
m.side_effect = [1, 2]
m()
>>> 1
m()
>>> 2
m()
>>> Traceback ()... StopIteration- W przypadku wyjątku, wyjątek zostanie rzucony:
m = Mock()
m.side_effect = ValueError
m()
>>> Traceback (...) ValueError- W przypadku funkcji, funkcja zostanie wywołana:
m = Mock()
m.side_effect = lambda x: x+2
m(10)
>>> 12Ważne różnice:
# 1. iterable
m = Mock()
m.foo.return_value = [1, 2, 3]
m.foo.side_effect = [1, 2, 3]
m.foo()
>>> [1, 2, 3]
m.foo()
>>> [1, 2, 3]
m.bar()
>>> 1
m.bar()
>>> 2
# 2. wyjątki
m.foo.return_value = ValueError
m.bar.side_effect = ValueError
m.foo()
>>> <class 'ValueError'>
m.bar()
>>> Traceback(...) ValueError
# 3. funkcje
m.foo.return_value = lambda: 777
m.bar.side_effect = lambda: 777
m.foo()
>>> <function <lambda> at 0x7f40a9fb27b8>
m.bar()
>>> 777Mocka możemy dostraczyć do klasy przez Wstrzykiwanie Zależności (powinniśmy robić to zawsze, gdy to możliwe) lub przy pomocy patchowania. Patchowanie to sposób dostarczania mocka, który polega na zamienieniu definicji klasy/funkcji (jest bardziej "globalne" niż wstrzykiwanie zaleźności). Patchować w pythonie możemy na trzy sposoby:
- przy pomocy dekoratora - wewnątrz udekorowanej metody (testu) funkcja
os.path.existszostanie podmieniona na mocka, który zwraca wartośćTrue. Dekorator tworzy argument (w tym przypadku:exists_mock), który przekazywany jest do testu i za pomocą którego możemy odwołać się do mocka.
@patch("os.path.exists", return_value=True)
def test_that_non_existing_file_is_not_deleted(self, exists_mock):
# ...
exists_mock.assert_called_once()- Przy pomocy menadżera kontekstu - w obrębie bloku
withpatchowana funkcja/klasa zostanie podmieniona.
with patch("os.path.exists", return_value=True) as exists_mock:
# ...
exists_mock.assert_called_once_with(non_existing_file)- Przy pomocy
patchera. W tym wypadku tworzymy obiekt, który uruchomi nam podmianę i przy pomocy którego ją zatrzymamy w momencie, kiedy mock przestanie być potrzebny (po teście) - odpowiedzialność za "odkręcenie" patchowania spada na programistę!
def setUp(self):
super().setUp()
patcher = mock.patch('core.transfer_operations.calculate_subtask_verification_time', return_value=1800) # wywołanie zwraca tzw. "patcher", który posłuży do uruchomienia i zatrzymania mocka
self.addCleanup(patcher.stop) # dodanie metody stopującej mocka (patcher.stop) do metod automatycznie uruchamianych po teście
patcher.start() # wystartowanie mockaMock to nie jedyna klasa implementująca mocki w Pythonie. Jest jeszcze, np. MagickMock, który jest "cięższą" wersją Mock, wzbogaconą o implementację tzw. metod magicznych (tych zaczynających i kończąsych się od "__").
Mock oraz patch mogą zostać wzbogacone u użycie argumentu spec=JakasKlasa. Powoduje to stworzenie mocka zwierającego wszystkie (mockowe) metody jak JakasKlasa. Domyślnie Mock tworzy w locie metody, które próbujemy na nim wywołać. Użycie spec sprawia, że na mocku możemy wywołać tylko metody określone w specyfikacji (w tym wypadku: te, które ma JakasKlasa).
class JaskasKlasa(object):
def foo(self):
return "foo"
m = Mock()
m.foo()
m.bar()
spec_mock = Mock(spec=JaskasKlasa)
spec_mock.foo()
spec_mock.bar()
>>> Traceback(...) AttributeError: Mock object has no attribute 'bar'Powyższy efekt można wzmocnić używając argumentu spec_set=JakasKlasa. Powoduje on, że nie można dynamicznie rozbudować mocka i każda próba dodania czegoś do "specyfikacji" mocka zakończy się wyjątkiem.
class JaskasKlasa(object):
def foo(self):
return "foo"
spec_mock = Mock(spec=JaskasKlasa)
spec_mock.foo()
spec_mock.bar = lambda: 777
spec_mock.bar()
spec_set_mock = Mock(spec_set=JaskasKlasa)
spec_set_mock.foo()
spec_set_mock.bar = lambda: 777
>>> Traceback(...) AttributeError: Mock object has no attribute 'bar'Jeszcze silniejsze "zabezpieczenie" mocka w kierunku zgodności z oryginałem daje użycie automatycznej specyfikacji. Robi się to przez funkcję create_autospec (lub dodanie opcji autopsec=True do patch). Powoduje ona, że metody mocka będą sprawdzone pod kątem zgodności ilości argumentów w stosunku do specyfikacji.
class JaskasKlasa(object):
def foo(self):
return "foo"
spec_mock = Mock(spec=JaskasKlasa)
spec_mock.foo(777)
autospec_mock = create_autospec(spec=JaskasKlasa)
autospec_mock.foo(777)
>>> Traceback(...) TypeError: too many positional argumentsAutospec-a można używać z opcją spec_set=True, która działa jak opisano powyżej/
Podsumowując - w testach najlepiej korzystać z opcji create_autospec z flagą spec_set=True. Sprawia to, że nasz mock jest najsztywniej związany ze specyfikacją prawdziwego obiektu, dzięki czemu zmiany w jego imlementacji (np. dodanie obowiązkowego parametru w mockowanej metodzie) uszkodzą testy i będziemy mogli je poprawić (zamiast otrzymywać tzw. "false positive").
więcej: screencasty z techtalków
żródła: