-
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)
>>> 12
Waż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()
>>> 777
Mocka 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.exists
zostanie 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
with
patchowana 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 mocka
Mock
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 arguments
Autospec-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: