Confitura 2023 - Testing the untestable

W ostatnio miałem okazje uczestniczyć w konferencji Confitura. Jedna z prezentacji okazała się szczególnie ciekawa.

Testing the untestable - patterns and use cases analysis

Opis prezentacji:

Autor prezentacji: Piotr Stawirej

Zaprezentuję, jak można uprościć aplikację i umożliwić jej testowanie poprzez analizę napotkanych przypadków i zastosowanych rozwiązań. Skupimy się na niestandardowych przypadkach oraz często powtarzalnych sytuacjach, które występują podczas naszej pracy. Nauczymy się jak testować problematyczne klasy, integrację z bibliotekami, frameworkami i usługami zewnętrznymi, jak skutecznie używać zaślepek (fakes), co mockować, a czego nie, jak testować aplikacje wielowątkowe, jak przyspieszać testy integracyjne, jak testować kod zależny od czasu i działania bez widocznego efektu, jak testować tolerancję na błędy oraz jak ponownie wykorzystać akceptacyjne testy jednostkowe jako testy integracyjne lub API. Omówimy, dlaczego warto stosować Property Based Testing dla Value Object’ów, jak testować obiekty mutowalne, zapis (persistence), czy testować logowanie, jak testować kod asynchroniczny. Poznamy przydatne heurystyki testowania oraz alternatywę dla testów wydajnościowych.

Wprowadzenie

Poniżej przedstawiam kilka moim zdaniem szczególnie istotnych aspektów przedstawionych w prezentacji.

Pojęcia

Fake

Fake to implementacja prostego obiektu, która udaje funkcjonalność rzeczywistego obiektu, ale w sposób uproszczony. Fake są często tworzone ręcznie jako proste implementacje interfejsów lub klas, które na potrzeby testów mają zachowywać się podobnie do prawdziwych obiektów.

Mock

Mock to obiekt, który symuluje zachowanie innych obiektów w systemie, umożliwiając kontrolowanie i sprawdzanie interakcji. Mocki są często tworzone przy użyciu bibliotek (np. Mockito) i pozwalają programistom na kontrolowanie, jak obiekty testowane współdziałają z innymi zależnościami.

Sposoby na testowanie problematycznych klas

1. Optymalizacja czasu wykonania testów:

Jednym z ciekawych problemów omawianych podczas niedawnej prezentacji był przypadek testowy, który restartował kontekst Springa przed każdą metodą testową, co skutkowało znacznym wydłużeniem czasu wykonania testów. Aby temu zaradzić, warto poszukać bardziej efektywnych sposobów przygotowania się do testów. W opisanym przykładzie zamiast restartu zastosowano metodę setup, która usuwała wszystkie wiersze z bazy danych, skracając czas wykonania z 27 sekund do kilkuset milisekund. Ta optymalizacja znacząco poprawiła cały proces testowania.

2. Wykorzystanie implementacji Fake zamiast prostych Mocków

Innym interesującym podejściem podkreślonym w prezentacji było stosowanie implementacji Fake jako alternatywy dla prostych Mocków. Implementacje Fake mają przewagę ponownego wykorzystania w wielu testach. Przykładem tego podejścia był proste repozytorium oparte na mapie.

3. Testy jednostkowe jako testy na wyższym poziomie

Niezwykle interesującym przypadkiem omówionym podczas prezentacji było wykorzystanie testów jednostkowych jako testów na wyższym poziomie. Proces ten polegał na wyodrębnieniu interfejsu z komponentu odpowiedzialnego za przypadki użycia (np. serwisu aplikacyjnego w kontekście Domain-Driven Design). Następnie napisano test jednostkowy, który korzystał tylko z Mocków lub implementacji Fake, aby sprawdzić funkcjonalność aplikacji. Ten sam test mógł zostać skonfigurowany tak, aby korzystał z prawdziwych implementacji, na przykład repozytorium osadzonego lub opartego na kontenerach testowych. Co więcej, ten sam test mógł być rozszerzony o testowanie interfejsu API poprzez zastąpienie komponentu implementującego przypadki użycia klientem API lub rozwiązaniem opartym na Selenium.

4. Zapewnienie obserwowalnych wyników testów

Czasami w testach można napotkać wyniki, które są trudne do obserwacji, co może prowadzić do niejasności lub błędnego przypuszczenia, że oczekiwane zachowanie nie zostało zaimplementowane. Jest to szczególnie trudne w przypadku podejścia Test-Driven Development (TDD). Aby temu zaradzić, warto jawnie uwzględnić oczekiwane zachowanie w teście, nawet jeśli nie jest ono bezpośrednio obserwowalne.

Linki do kodu źródłowego i prezentacji

  • https://github.com/stawirej/financial-system
  • https://amazingcode.pl/talks/testing-the-untestable.pdf

Podsumowanie

Testowanie oprogramowania może być wyzwaniem, ale zastosowanie odpowiednich strategii może znacznie poprawić efektywność i jakość procesu testowania. Optymalizacja czasu wykonania testów, wykorzystanie implementacji Fake, testowanie na różnych poziomach abstrakcji i zapewnienie obserwowalności wyników to tylko kilka z wielu przydatnych strategii. Mając świadomość tych wyzwań i stosując odpowiednie metody, programiści mogą skutecznie radzić sobie z trudnościami związanymi z testowaniem i dostarczać lepsze oprogramowanie.