Siedem grzechów głównych testowania jednostkowego

devstyle.pl 1 rok temu

Poniżej znajdziesz listę naszych subiektywnych 7 największych błędów, które programiści popełniają w temacie testów jednostkowych. Możliwych problemów jest oczywiście o wiele więcej, ale dziś skupimy się na takich najważniejszych – pod względem konsekwencji albo częstotliwości występowania.

Autorką tekstu jest Olga Maciaszek-Sharma, a listę kompletował również Marcin Grzejszczak. Oboje są Mentorami w bestsellerowym Szkoleniu SmartTesting, do którego serdecznie zapraszamy! Zapisy realizowane są do środy 20 września, do godziny 21:00.

Zobacz szczegóły na SmartTesting.pl »!

1. Brak jakichkolwiek testów

Pierwszym, dość oczywistym może grzechem jest brak jakichkolwiek testów, w tym testów jednostkowych. Niestety wciąż jeszcze zdarzają się zespoły, które uważają, że – z różnych powodów – nie muszą wcale pisać testów. Niektórzy myślą, że są w stanie przetestować wszystko w głowie… i może niektóre są, choć w to wątpię.

Inni uważają, że wystarczy przeklikać nową funkcjonalność na środowisku lokalnym. Prawda jest jednak taka, że choćby jeżeli mamy geniusza będącego w stanie „przetestować w głowie” wraz z innymi członkami zespołu bardzo dokładnie i pieczołowicie weryfikującymi działanie aplikacji na środowisku lokalnym, to nie wystarczy. Przecież inne osoby z tego samego zespołu mogą nie zrozumieć, o co chodziło w danej funkcjonalności i wprowadzić niepoprawne zmiany – co spowoduje regresję. Regresję, która nie zostanie wychwycona i prawdopodobnie trafi na produkcję.

Inne zespoły rozumieją potrzebę testowania, ale i tak nie testują, ponieważ biznes nie wie, dlaczego testy są potrzebne. Napięte harmonogramy wywierają presję, by nie poświęcać czasu w testy. Jednak z naszego doświadczenia wynika, że biznes można przekonać rozmawiając o testach od strony finansowej. Prawda jest taka, że brak testów na dłuższą metę powoduje koszty, a nie oszczędności. Koszty spowodowane niewykrytymi odpowiednio wcześnie błędami w systemach IT są niebotyczne, a naprawienie błędów wykrytych w początkowych fazach tworzenia systemu jest 100x tańsze, niż w fazie utrzymania. Wprost wynika z tego, iż testy – a szczególnie testy jednostkowe, będące niezwykle istotnym źródłem feedbacku już na bardzo wczesnym etapie tworzenia systemu – przyczyniają się w praktyce do bardzo dużych oszczędności.

Testy bardzo usprawniają też pracę całego zespołu nie tylko przy dodawaniu nowych funkcjonalności, ale też na etapie utrzymania. Zespoły często poświęcają znacznie więcej czasu w zmiany istniejącego kodu niż dodawanie nowego, a dobrze napisane testy pozwalają to robić znacznie łatwiej, szybciej i w sposób mniej zachowawczy. Gdy mamy dobre testy, bardzo gwałtownie wykryjemy ewentualne regresje. Testy jednostkowe umożliwiają też bardzo szybkie debugowanie małych fragmentów kodu, bez konieczności uruchamiania całej aplikacji czy kontekstu.

Testy stanowią również żywą dokumentację projektu. Jeżeli mamy dobre testy, i jeszcze do tego dobrze nazwane, to – w przeciwieństwie do tradycyjnej, pisanej ręcznie dokumentacji – opisują one to, jak system faktycznie działa, a nie tylko jak chcielibyśmy, żeby działał. Co więcej, są narzędzia, przy pomocy których możemy wygenerować dokumentację z istniejących testów.

2. Testowanie absolutnie wszystkiego

Z drugiej strony, gdy zespół podejdzie trochę zbyt entuzjastycznie do kwestii testowania (szczególnie w testach jednostkowych) zdarzają się sytuacje, że próbujemy testować zbyt wiele. Można tu wymienić takie kwestie jak testowanie jednostkowe funkcjonalności / kodu wygenerowanego przez zewnętrzne narzędzia bądź frameworki albo metod dostarczanych przez zewnętrzne biblioteki. W tego typu sytuacjach często wystarczy weryfikacja danej funkcjonalności z poziomu testów integracyjnych.

Podobnie, nie powinniśmy próbować testować metod prywatnych. Często gdy wydaje nam się, że weryfikacja działania jakiejś metody prywatnej zasługuje na oddzielny test, może to świadczyć o tym, że jest problem w strukturze klas naszej aplikacji i metoda ta powinna być na przykład wyekstrahowana do metody pakietowej nowej, bardziej wyspecjalizowanej klasy.

Zdarzają się też przypadki, choć bardzo szczególne i ograniczone, na przykład gdy tworzymy na gwałtownie jakiś prototyp, który nie będzie używany produkcyjnie lub banalnie prostą aplikację, na przykład typu CRUD, bez dodatkowej logiki biznesowej, kiedy można rozważać rezygnację z pisania testów w ogóle.

3. Testy, które nic nie weryfikują

Wbrew pozorom, testy, które nic nie weryfikują, zdarzają się częściej, niż mogłoby się wydawać. Pierwszy ich rodzaj, najłatwiejszy do wykrycia i poprawienia to testy, które nie kończą się jednoznacznym wynikiem negatywnym lub pozytywnym. Każdy test, który w ogóle jest testem, ma asercję(/e), która jasno wskazuje na to czy test przeszedł. Testy bez asercji nie kończą się jednoznacznym wynikiem (na przykład tylko coś logując). A testy wymagające dodatkowych manualnych kroków po wykonaniu (jak, na przykład, weryfikacja czegoś w bazie danych), nie powinny pojawiać się w naszych projektach.

Trudniejsze do wykrycia są testy, które przechodzą tylko dlatego, że weryfikowany wynik jest zbieżny z wartościami domyślnymi. Albo choćby takie, które w istocie wcale nie powodują (na przykład ze względu na błędną konfigurację) wywołania pod spodem testowanych metod (funkcji). Zdarza się też, że asercje zostały błędnie skonstruowane.

Jest kilka prostych rzeczy, które można zrobić, żeby uchronić się przed takimi sytuacjami. Warto pracując nad testem na chwilę „zepsuć dane”, czyli testując na przykład przypadek pozytywny, zmienić dane wejściowe (np. użytkownik, któremu przysługuje kredyt) na takie, dla których przypadek powinien być negatywny (np. użytkownik ze złą historią kredytową) i zobaczyć czy wtedy nasz test nie przejdzie. Inna weryfikacja czy test faktycznie coś testuje, to sprawdzenie co się stanie jeżeli odwrócimy asercje (np. z `isEqualTo` na `isNotEqualTo`) lub na chwilę zakomentujemy kawałek logiki biznesowej, którą chcielibyśmy przetestować. Jeżeli w tych sytuacjach test przez cały czas będzie przechodził, to prawdopodobnie nic on w praktyce nie testuje.

Warto też weryfikować liczbę wykonanych testów w outpucie naszego narzędzia do budowania. Może się zdarzyć, że pomimo poprawnie działających testów uruchamianych w naszym środowisku uruchomieniowym, to z powodu złej konfiguracji narzędzia do budowania, bądź niezastosowania się do danej konwencji przy nazywaniu naszych klas czy metod (funkcji) testowych, nie zostaną one w ogóle uruchomione w najważniejszym momencie, czyli w procesie budowania naszej aplikacji.

4. Testy, które psują się przy każdej zmianie

Odwrotnością testów, które nic nie weryfikują, są testy weryfikujące zbyt wiele, prowadząc do tzw. zabetonowania aplikacji testami. Wtedy przy każdej, choćby najmniejszej zmianie w naszej aplikacji, przestają przechodzić dziesiątki testów. Co się wtedy dzieje? zwykle zespoły przestają w ogóle uruchamiać testy i tracą wszelkie korzyści płynące z ich posiadania.

Najczęściej do „zabetonowania” dochodzi wtedy gdy używamy zbyt szczegółowych asercji. Na przykład w teście sprawdzającym, że dana metoda (funkcja) została uruchomiona, będziemy sprawdzać, że została uruchomiona np. z konkretnym Stringiem jako argumentem. Czy to znaczy, że nigdy nie powinniśmy zweryfikować tego konkretnego Stringa? Nie, jeżeli wartość jest istotna dla naszej logiki biznesowej, to możemy chcieć ją zweryfikować, ale… w jednym teście, a nie w dwudziestu.

Podobnie, problem może się pojawić, gdy zamiast weryfikować efekty danej operacji, staramy się sprawdzać jak dokładnie została ona, krok po kroku, zrealizowana – czyli weryfikujemy szczegóły implementacyjne, zamiast rezultatu.

5. Nieczytelne testy

Jednym z powodów, dla których testy bywają określane jako “trudne” lub “zbędne”, jest kwestia ich czytelności. Często zdarza się, że zespół nie dokłada takich samych starań w zakresie czytelności kodu testowego jak w przypadku pisania kodu produkcyjnego, przez co testy trudno się czyta i trudno refaktoruje.

Problemem bywa niejasne nazewnictwo pól i zmiennych, weryfikacja zbyt wielu rzeczy w jednym teście, brak jasnego podziału testu na sekcje „Arrange”, „Act”, „Assert”, brak wydzielenia setupu bądź przygotowania danych testowych do oddzielnych metod. Często problematyczne jest też stosowanie asercji na poziomie szczegółów implementacyjnych, zamiast wykorzystania np. wzorca „AssertObject” do utworzenia asercji na poziomie logiki biznesowej.

Negatywny wpływ na czytelność testów często ma brak spójności i stosowania konwencji, np. jeżeli chodzi o nazewnictwo klas i metod (funkcji) czy wykorzystywane narzędzia i biblioteki testowe.

6. Stubowanie i mockowanie wszystkiego

W środowisku od dawna trwają spory dotyczące tego, czy stubowanie i mockowanie (używanie zaślepek i narzędzi do weryfikacji wywołań metod/ funkcji) w testach jednostkowych jest dobrą praktyką. Stanowiska i argumentację obydwu stron sporu przedstawiamy bardziej szczegółowo w szkoleniu SmartTesting ». Naszym zdaniem nierzadko warto je stosować dla lepszej izolacji testowanych obiektów, co może polepszyć czytelność testów i ułatwić życie w sytuacji zbyt skomplikowanego setupu testu. Ze stubowaniem i mockowaniem zdecydowanie można jednak przesadzić, osiągając wręcz przeciwny efekt i negatywnie wpływając na czytelność. Może się też zdarzyć, iż test choćby nie będzie odpowiednio weryfikował tego, na czym nam zależało.

Jedną z rzeczy, która praktycznie zawsze jest złym pomysłem, jest stubowanie lub mockowanie metod z bibliotek i narzędzi nieutrzymywanych przez nas. Jest wtedy duża szansa, że nie zorientujemy się, gdy autorzy biblioteki postanowią zmienić implementację i nasze stuby przestaną odzwierciedlać jej zachowanie. To z kolei może prowadzić do sytuacji, w której nasze testy przestaną w rzeczywistości weryfikować jak aplikacja zachowuje się w interakcji z tą zewnętrzną biblioteką.

Kolejnym problemem jest stubowanie obiektów, które bardzo łatwo byłoby po prostu utworzyć, np. Stringów, kolekcji czy wyników wywołań metod użytkowych (“utils”) standardowych bibliotek języków. Na przykład zamiast stubować metodę zwracającą informację czy String jest pusty, lepiej po prostu przekazać pustego Stringa. Tego typu nadmierne stubowanie sprawia, że nasze testy robią się niepotrzebnie skomplikowane i mało czytelne.

Podobnie jak przy testowaniu metod (funkcji) prywatnych, nie jest najlepszym pomysłem ich stubowanie czy mockowanie. Gdy widzimy potrzebę mockowania czy stubowania takich metod (funkcji), to zwykle świadczy to o tym, że albo można by poprawić strukturę naszego kodu produkcyjnego (o czym pisałam wyżej) albo że próbujemy zbyt szczegółowo weryfikować implementację – a to prawdopodobnie doprowadzi do „betonowania” aplikacji (o czym też była mowa już wcześniej).

Warto wziąć tu pod uwagę także tak zwane „Prawo Demeter dla mocków i stubów”. Zgodnie z nim nasze stuby nie powinny zwracać innych stubów bądź mocków. Czasami trudno jest uniknąć takiej sytuacji, ale najlepiej byłoby postarać się ograniczyć używanie takiego setupu do minimum.

7. Zbyt wolne testy

Last, but not least… żeby testy (szczególnie jednostkowe) były regularnie uruchamiane przez zespół i nie ograniczały jego produktywności, powinny być one szybkie. A dobrze napisane testy faktycznie takie są. Jest jednak kilka często spotykanych błędów w tym aspekcie.

Jednym z nich jest podnoszenie kontekstu frameworka aplikacyjnego bądź kontenerów z bazami danych (lub innymi zewnętrznymi komponentami). O ile jest to normalne przy testach integracyjnych, o tyle nie powinno mieć miejsca w testach jednostkowych. Często bywa to nadużywane szczególnie w sytuacji użycia frameworków bazujących na IoC (odwróceniu kontroli), kiedy to w polach klas pojawiają się obiekty od razu łączące się do zewnętrznych komponentów. Najłatwiejszym rozwiązaniem w takiej sytuacji bywa wykorzystanie interfejsów i przekazywanie do testów jednostkowych implementacji zwracających dane w znacznie szybszy sposób. Na przykład pole z obiektem pobierającym i zwracającym dane z bazy, możemy w teście wypełnić obiektem implementującym ten sam interfejs, ale pobierającym dane ze zwykłej kolekcji.

Często też problemem jest zbyt długie oczekiwanie w testach. Nierzadko zdarza się, że musimy chwilę odczekać, żeby móc coś zweryfikować. Jeżeli w takiej sytuacji ustawimy sztywny czas oczekiwania, to musi on być tak długi, jak długo może maksymalnie zająć realizacja oczekiwanej operacji. Niepotrzebnie wydłuża to testy. Zamiast tego, lepiej jest użyć pomocniczego narzędzia. Np. w Javie mamy Awaitility, które w krótkich interwałach będzie ponawiać próby weryfikacji, czy wynik już jest dostępny.

Więcej?

Po duuuużo więcej wiedzy zapraszamy do SmartTesting »! Do zobaczenia!

Idź do oryginalnego materiału