Skip to content

TECH TALK 3 MOCKOWANIE

Piotr Dybowski edited this page Jan 24, 2019 · 2 revisions

Co to jest Mock

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:
  1. 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
  1. W przypadku wyjątku, wyjątek zostanie rzucony:
m = Mock()
m.side_effect = ValueError
m()
>>> Traceback (...) ValueError
  1. 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

Patch a Mock (kiedy jedno, kiedy drugie)

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:

  1. 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()
  1. 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)
  1. 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, MagicMock

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 "__").

spec - specyfikacja mocka

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'

spec_set - zamrożenie mocka

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'

autospec - czego chcieć więcej

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/

Dobre rady

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:

Wstęp do mockowania

Testy jednostkowe z mockiem

Wprowadzenie do mockowania w pythonie