Mocki – radzenie sobie z zależnościami w testach

Wprowadzenie do Test Driven Development - wszystkie wpisy

Testowanie kodu, który nie wykorzystuje zewnętrznych zależności jest stosunkowo proste. W większości przypadków testowany moduł współpracuje jednak z innymi elementami systemu.  Stawia to przed testami dwa wyzwania – po pierwsze powinny poprawnie działać, a po drugie sprawdzać poprawność tej współpracy. Nie jest to zadanie proste, a zewnętrzne zależności są jednym z głównych czynników utrudniających testowanie.

Dlaczego potrzebujemy mocków

Aby radzić sobie z zależnościami posługujemy się mockami, czyli dublerami zastępującymi zależny kod produkcyjny na czas testów. W klasycznym developmencie również często korzystamy z takich dublerów jako rozwiązanie tymczasowe pozwalające sprawdzić nam pewne ścieżki programu i kasujemy je po osiągnięciu celu. W TDD mocki są utrzymywane przez cały cykl życia kodu produkcyjnego w celu ułatwienia automatyzacji testów.

Z mocków korzystamy, kiedy wykorzystanie modułu produkcyjnego w teście jest problematyczne. Jeżeli mamy możliwość prostego użycia takiego modułu zawsze powinniśmy wybierać takie rozwiązanie. Często jednak okazuje się, że moduł zależności wymaga do działania kolejnych zależności, które z kolei wymagają jeszcze następnych, tworząc w ten sposób całą sieć. Szybko się może okazać, że taka sieć obejmuje prawie cały projekt.

Dlatego właśnie na czas testów automatycznych problematyczne zależności zastępujemy dublerami. Aby proces ten przebiegał bezboleśnie, system powinien być zaprojektowany z myślą o testowalności. Czyli powinien mieć dobrze zdefiniowane interfejsy, a szczegóły implementacyjne powinny zostać ukryte za warstwą abstrakcji.

Kiedy używać mocków

Istnieje kilka standardowych przypadków, kiedy niezbędne jest skorzystanie z mocka. Są to:

  • Interakcja ze sprzętem – potrzeba uruchamiania testu na docelowym sprzęcie powoduje znaczne spowolnienie wykonywania zestawu testów. Poza tym taki sprzęt nie zawsze jest dostępny, a korzystanie z niego może być drogie lub uciążliwe.
  • Symulowanie ciężkiego do uzyskania zachowania – testy powinny pokrywać różnego rodzaju przypadki brzegowe i wyjątki. Mając do dyspozycji standardowe punkty wejścia może być ciężko wywołać takie zachowania. Dlatego za pomocą mocków łatwiej nam będzie wywołać przekłamany odczyt z dysku, czy zgubienie pakietu na sieci.
  • Przyspieszenie wolno działającego bloku – w TDD testy automatyczne powinny od razu dawać wyniki. Często więc musimy przyspieszyć operacje takie jak odpytywanie serwera webowego, interakcje z bazą danych, czy odczyt z zewnętrznej pamięci.
  • Zależność od czegoś zmiennego – testy powinny być powtarzalne. Nie mogą więc bazować na czymś, co będzie różniło się pomiędzy wywołaniami jak np. liczby losowe, czy data.
  • Zależności trudne do konfiguracji – korzystanie bezpośrednio z zależności, którą przed każdym testem należy w skomplikowany sposób konfigurować dodaje dużo trudnego do zrozumienia kodu i czyni testy wrażliwymi na zmiany.
  • Zależność od elementu, do którego nie ma dostępu – dzięki mockom możemy tworzyć testy do modułów komunikujących się z zależnościami, które nie zostały jeszcze zaimplementowane. Wystarczy, że mają zdefiniowany interfejs. Innym przykładem jest interakcja z aplikacją działającą na produkcji, z którą nie możemy się komunikować podczas automatycznego testu.

Budowa mocka

Mock implementuje dwa interfejsy. Jeden z nich jest wykorzystywany przez testowany kod produkcyjny i zawiera te same funkcje co kod mockowanej zależności. Drugi interfejs jest wykorzystywany przez kod testowy do konfiguracji mocka i sprawdzenia poprawności interakcji kodu produkcyjnego z mockowaną funkcją.

Rodzaje mocków

Możemy wyróżnić kilka rodzajów mocków różniących się stopniem skomplikowania oraz zastosowaniem. Gerard Meszaros w swojej książce „xUnit Test Patterns” wyróżnia następujące rodzaje mocków:

  • Test dummy – pusta funkcja znajdująca się w kodzie tylko po to, aby uniknąć błędu kompilacji.
  • Test stub – mockowana funkcja zwraca konkretną wartość ustawioną podczas konfiguracji mocka.
  • Test spy – podobnie jak test stub zwraca wartość, a także zapamiętuje parametry z jakimi mockowana funkcja została wywołana w celu ich późniejszego sprawdzenia w teście.
  • Mock object – potrafi obsługiwać wielokrotne wywołanie mockowanej funkcji w jednym teście. Dla każdego wywołania może zwracać inną wartość i pozwala sprawdzić poprawność parametrów z jakimi funkcja za każdym razem była wywołana. Potrafi również sprawdzać, czy poszczególne mockowane funkcje zostały wywołane w odpowiedniej sekwencji.
  • Fake object – w mockowanej funkcji zaimplementowana jest pewna logika symulująca działanie właściwego modułu w określonych warunkach.
  • Exploding fake – wejście do mockowanej funkcji powoduje natychmiastowy fail testu, jeśli są spełnione odpowiednie warunki.

Sposoby mockowania

W językach obiektowych do podstawienia mocka pod interfejs zależności wykorzystywany jest polimorfizm. Implementujemy ten sam interfejs co klasa produkcyjna i podstawiamy go do testu. W C jest to nieco trudniejsze do osiągnięcia i stosuje się następujące techniki:

  • Podmiana funkcji w czasie linkowania – zamiast implementacji produkcyjnej linkujemy implementację mocka. Ten sam mock wykorzystywany jest w każdym teście dla tego builda.
  • Wykorzystanie wskaźników do funkcji – mockowany interfejs składa się z wskaźników do funkcji, do których przypisane są domyślnie produkcyjne implementacje. Test może przypisywać do nich implementacje mocków. Dzięki temu w jednym buildzie testowym możemy wykorzystać różne implementacje mocka. Wadą tego rozwiązania jest zaśmiecenie headera produkcyjnego dodatkowymi deklaracjami i zmniejszenie czytelności kodu przez użycie wskaźników na funkcje. Poza tym kompilator nie jest w stanie wtedy przeprowadzić pewnych optymalizacji.
  • Wykorzystanie preprocesora – wykorzystanie dyrektywy define i kompilacji warunkowej przy pomocy dyrektywy ifdef. Możemy dzięki niej wykluczyć z kompilacji część includów oraz podmienić znaczenie symboli. Ten sposób powinien być wykorzystywany w ostateczności, ponieważ preprocesor sprawi, że kod używany w teście będzie tak naprawdę inny niż ten wykorzystywany w buildzie produkcyjnym.
  • Łączenie powyższych technik – możemy na przykład zaimplementować mocka podmieniając funkcję podczas linkowania, a w ciele tej funkcji konfigurować różne wskaźniki na funkcje używane w konkretnych testach.

Pisanie mocków samemu vs gotowe biblioteki

Istnieje wiele gotowych frameworków generujących dla nas mocki w sposób automatyczny. Powstaje więc pytanie, czy lepiej korzystać z takiej biblioteki, czy tworzyć mocki samemu. Odpowiedź nie jest taka oczywista.

Na początku nauki TDD i unit testów polecam pisać mocki samemu. Dzięki temu możemy lepiej zrozumieć, co się dzieje. Nie wywołujemy jakiejś biblioteki, która magicznie robi wszystko za nas. W ten sposób szybciej się nauczymy pisać dobre testy, a przede wszystkim projektować dobre interfejsy.

Najczęściej wykorzystywane rodzaje mocków, czyli Test stub i Test spy są dosyć proste w implementacji i tworzenie ich samemu nie powinno powodować zbyt wielu problemów. Jednak Mock objecty mają nietrywialną implementację i pisanie ich samemu może być czasochłonne oraz podatne na błędy. W takim wypadku lepiej więc skorzystać z gotowej biblioteki. Pisząc mocki samemu mamy większą elastyczność – możemy sami zdecydować jakie funkcjonalności chcemy zawrzeć w naszym mocku. Jednak pisanie wielu mocków w ten sposób również bywa czasochłonne i warto wspomóc się gotową biblioteką.

Bob Martin na przykład zaleca pisanie prostych mocków samemu (link do artykułu). Dzięki temu nasz kod jest bardziej czytelny, unikamy zależności od frameworka i często skracamy czas kompilacji testów. Framework powinien nas wspomagać przy pisaniu trudniejszych mocków, dzięki czemu oszczędzimy czas i nie narazimy się na błędy.

Nazewnictwo

W tym artykule używałem nazwy mock dla wszystkich rodzajów podmienionej implementacji testowej. W różnych materiałach o unit testach można się spotkać z innym nazewnictwem, gdzie mock oznacza Mock object, a na inne rodzaje mówi się Stubs, albo Test dobules. Polecam artykuł Martina Fowlera na ten temat.

Podsumowanie

Mocki to zastępcze implementacje zależności wykorzystywane podczas testów automatycznych. Mają za zadanie kontrolowanie granic pomiędzy kodem produkcyjnym a testem.  Poza ograniczeniem zależności służą do zadawania wejść i odczytywania wyjść testowanego modułu.

W artykule omówiłem dlaczego mocki są potrzebne do testowania, w jakich przypadkach ich używać oraz jakie rodzaje mocków możemy wyróżnić. Przybliżyłem również sposoby podstawiania mocków w języku C, gdzie jest to trudniejsze niż w przypadku języków obiektowych. Omówiłem także kiedy warto skorzystać z gotowych bibliotek do mockowania, a kiedy lepiej napisać je samemu.

Wprowadzenie do Test Driven Development - Nawigacja

2 Comments

  1. Jakie frameworki polecasz do testów w C? może przygotujesz jakiś artykuł z przykładową implementacją. Pełno tego w OOP ale w embedded mało informacji o tym.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *