Blog
4 kroki ku dobrze odizolowanym testom
Tagi: dependency injection, tdd
Jednym z wyznaczników Testów Jednostkowych jest izolacja. Implikuje ją nawet sama nazwa “Jednostkowe”. Dobrze odizolowane testy ułatwiają lokalizację błędów, są mniej kruche i szybciej się wykonują.
Ale czy Twoje testy rzeczywiście są dobrze odizolowane? I czy są łatwe do odizolowania, czy też ich konfiguracja jest bolesna?
Istnieje kilka progresywnych kroków, które możesz podjąć by poprawić izolację swoich testów:
PODSTAWOWA TECHNIKA IZOLACJI: Test Doubles plus Dependency Injection.
Podstawowym wzorcem związanym z izolacją testów są Test Doubles: mocks, spies, stubs oraz dummy objects. Możesz próbować zmniejszać ilość zależności, nie jest to jednak możliwe by nietrywialny system nie miał ich w ogóle. Podmiana zależności na Test Doubles to jedyny sposób, by w pełni odizolować jednostkę kodu.
Test Doubles są nierozerwalnie połączone z wzorcem Dependency Injection (“Wstrzykiwanie Zależności”). Zależność nie może być podmieniona na na Test Double, jeśli tworzenie jej instancji jest zaszyte na sztywno wewnątrz testowanej jednostki kodu 1. Tworzenie instancji zależności poza testowaną jednostką i wstrzykiwanie ich do niej umożliwia taką podmianę.
ALE: Istnieją rzeczy, których nie jestem w stanie “sfałszować”!
Niestety, pewne kostrukcje w kodzie są trudne do “sfałszowania”. Parę przykładów: singleton-y, statyczne metody albo konkretne klasy używane do typowania parametrów. Tego typu konstrukcje nie chcą współgrać z frameworkami do mockowania. Wymagają one trudnych do skonfigurowania i późniejszego utrzymywania rozwiązań takich jak ręczna implementacja “fałszywych” objektów albo subclass-owanie zależności testowanej jednostki 2.
IDĄC DALEJ: Unikaj kodu trudnego do “sfałszowania”.
Na szczęście, trudny do “sfałszowania” kod prawie zawsze ma jakieś łatwiejsze do “sfałszowania” alternatywy:
- Zamiast używać konkretnych klas do typowania parametrów, można do tego użyć interfejsów.
- Statyczne metody mogą zostać wydzielone do osobnych klas pomocniczych, jako metody instancji.
- Singletony mogą być zastąpione połączeniem dynamicznych klas oraz dodatkowego, zewnętrznego mechanizmu kontrolującego ilość tworzonych instancji.
Taki “łatwy do sfałszowania” design nie tylko ułatwi izolację Twojej jednoski kodu, ale też zaowocuje lepszym kodem, łatwiejszym do utrzymania – jest więc to podwójnie korzystna sytuacja.
ALE: Konfiguracja Test Doubles jest taka skomplikowana!
Zastępując wszystkie zakodowane na sztywno zależności użyciem Dependency Injection oraz eliminując wszystkie “nie fałszowalne” konstrukcje w kodzie, sprawiliśmy, że pełna izolacja testowanej jednostki kodu stała się możliwa. Możliwa nie równa się jednak łatwej.
Spotkałem się już z testami wymagającymi 4-5 stub-ów o 3 poziomach zagnieżdżenia każdy, by w pełni odizolować testowaną jednostkę kodu. Pisanie i utrzymywanie takiej konfiguracji dla testów nie jest zabawne, a jej kruchość podważa większość korzyści wynikających z izolacji.
IDĄC DALEJ: Redukuj zależności w Twoim kodzie.
Podstawową techniką izolacji jednoski kodu jest “sfałszowanie” zależności tej jednoski. Jednak jeszcze lepszą techniką jest eliminacja tych zależności, tak by w ogóle nie trzeba ich było “fałszować”.
Istnieje wiele zasad i wzorców projektowych, które pomagają eliminować zależności albo czynią je “płytszymi”. Kilka przykładów:
- Prawo Demeter
- Single Level of Abstraction Principle
- Single Responsibility Principle
- Command-Query Separation
- Fasada
- Adapter
Najważniejsze, aby pamiętać, że izolacji testów nie osiąga się jedynie – ani nawet w przeważającej części – w testach; osiąga się ją przede wszystkim poprzez czysty design kodu, który jest testowany.
ALE: Mój kod jest ściśle powiązany z moim frameworkiem!
Osiągnięcie izolacji poprzez odpowiedni design kodu, który jest testowany, brzmi dobrze w teorii, jednak kod nigdy nie istnieje w próżni – jest on powiązany z architekturą sysemu. Szczegóły implementacji różnych warstw architekturalnych czy frameworków mają tendencję do przeciekania do Twojego kodu domenowego. Twoje klasy często dziedziczą po bazowych klasach frameworka ORM, są splecione z warstwą routingu albo prezentacyjną frameworka MVC itp. To sprawia, że są one trudne do odizolowania.
IDĄC DALEJ: Stosuj ogólną architekturę systemu ułatwiającą izolację.
Architektura nie musi być aż tak inwazyjna. Istnieje wiele architektur, które zapewniają czystą izolację pomiędzy warstwami infrastrukturalnymi i logiką domenową. Kilka przykładów:
- Hexagonal Architecture Alistaira Cockburna
- Clean Architecture Boba Martina
- Onion Architecture Jeffreya Palermo
Co istotne, możesz skorzystać z dobrodziejstw tego typu architektury nawet jeśli Twój framework nie stosuje się do jej zasad. Architektury podobne do wspomnianych powyżej pomagają Ci odizolować się od frameworka, wyplątać go ze swojego kodu i wypchnąć na obrzeża Twojego systemu. To z kolei daje Ci swobodę na pisanie kodu w sposób, który wspiera dobrą izolację testów.
BONUS: Najprostsza heurystyka izolacji testów.
Dążenie ku dobrze odizolowanym testom to stopniowa progresja:
- używaj Test Doubles i Dependency Injection
- podmień trudny do “sfałszowania” kod na jakieś inne kostrukcje
- uzywaj wzorców projektowych, które wspierają izolację jednostki kodu
- stosuj architekturę systemu, która uwalnia Cię od powiązań z architektonicznym “rusztowaniem”
Na każdym z poziomów można wiele zrobić – ten artykuł prześlizguje się jedynie po powierzchni tematu. Skąd masz wiedzieć, że zrobiłeś dostatecznie dużo? Czy że w ogóle zmierzasz we właściwym kierunku? I na którym z poziomów progresji powinieneś się skupić w tej chwili?
Najprostsza heurystyka, która może pomóc Ci znaleźć odpowiedź, to: Konfiguracja testów powinna być bezbolesna.
Jeśli konfiguracja jest trudna albo skomplikowana, zwróć uwagę, jaka może być tego przyczyna: Czy sprawia to konkretna metoda? Ogólny design danej klasy? Zbyt silne powiązanie z frameworkiem? Spróbuj zidentyfikować “winnego” i odizolować go od testowanego kodu, używając technik podobnych do opisanych w tym poście. Powtarzaj ten proces dopóki konfiguracja testów nie przestanie Ci sprawiać problemów.
Jakich technik Ty używasz by odizolować testy jednostkowe? Co najskuteczniej działa w Twoim przypadku? Podziel się swoim doświadczeniem w komentarzach poniżej!
- W językach dynamicznych takich jak JavaScript czy Ruby, jest możliwe by “sfałszować” zaszyte na sztywno zależności (n.p. poprzez stubbing konstruktorów). Nie jest to jednak dobrą praktyką. Łamie to enkapsulację i powoduje, że szczegóły implementacji przeciekają do testów, co sprawia, że testy stają się kruche (Napiszę na ten temat więcej w moim kolejnym poście). ↩
- Ponownie, zależy to od używanego języka i frameworka Dependency Injection. Jednakże praktycznie każdy język ma swoje dziwactwa, więc choć konkretne przykłady, jakie podaję, mogą nie przystawać do Twojej sytuacji, ogólne podejście pozostaje bez zmian. ↩