Dobry zestaw testów jest kluczowym aspektem dla deweloperów, po to by działali efektywniej i tworzyli stabilnie działające oprogramowanie. Nadrzędnymi wartościami są jednak płynące z testów korzyści biznesowe:
- Dostarczanie odpowiedniej funkcjonalności o dobrej jakości → ograniczenie liczby błędów
- Krótki time-to-market → szybko działające testy i proces wydawania oprogramowania
- Brak utraty reputacji → automatyzacja regresji (brak nowych błędów)
Z tego artykułu dowiesz się
- Od czego zależy koszt wykonania testu
- Czym jest piramida testów
- O rodzajach testów: jednostkowych, komponentowych, integracyjnych, systemowych i testach e2e
- Jak efektywnie uruchamiać poszczególne poziomy testów
Aby spełnić przesłanki biznesowe, należy zastanowić się nad kosztem testowania w stosunku do osiąganej korzyści. Koszt napisania testu i jego podstaw (infrastruktura, ekstrahowanie części wspólnych) to koszt jednorazowy. Natomiast czas działania testów i ich stabilność wpływają na time-to-market, co jest to kosztem ponoszonym cyklicznie. Idealnie, gdy po zmianie w projekcie otrzymujemy feedback z testów pokrywających kod produkcyjny tak szybko, jak to jest możliwe.
Dlatego ważne jest podzielenie testów ze względu na łatwość i skuteczność wykonania testu oraz czas jego wykonania.
Piguła wiedzy o najlepszych praktykach testowania w Java
Pobierz za darmo książkę 100 stron o technikach testowania w Java
Od czego zależy koszt wykonania testu?
- Przede wszystkim od tego, czy jest automatyczny, czy manualny
- Od tego, czy jest stabilny
Jeżeli pomiędzy uruchomieniami raport testu zmienia się w czasie bez zmiany kodu, jest to moment, kiedy wymaga to analizy i zaangażowania kogoś z zespołu. Uznajemy test jako niestabilny. Im więcej komunikacji z otoczeniem zewnętrznym (np. serwisami, które mogą być niedostępne) i infrastrukturą (wyścigi lub czekanie na odpowiedzi) – tym więcej potencjalnych błędów i dłuższy czas oczekiwania na wynik. - Od komunikacji I/O i czasu przetwarzania
Komunikacja z innymi serwisami, odpytywanie bazy danych, obserwowanie naszej działającej aplikacji „z zewnątrz” zajmuje czas. - Od szybkości jego działania – im większa izolacja, tym szybszy czas wykonania
Jeżeli test nie wymaga: kontaktu z bazą danych (nawet in-memory), testowania z użyciem protokołu (np. symulacja żądania HTTP), wtedy działa szybciej. Testujemy małe, niezależne od siebie fragmenty. Pułapką jednak może być to, że po złożeniu małych fragmentów aplikacja jako całość nie działa poprawnie – zła konfiguracja lub problemy infrastrukturalne.
Czym jest piramida testów
Piramida testów to zróżnicowanie ilości testów w zależności od ich typów. Im wykonanie testu jest tańsze, szybsze, bardziej stabilne – tym powinno ich być więcej i znajdują się na niższym szczeblu drabiny, stanowiąc jej fundament. Jeżeli wykonanie testu zajmuje więcej czasu, jest mniej stabilne, angażuje pracę ludzką, tym tego typu testów jest mniej i są na czubku piramidy.
Ideą piramidy testów jest posiadanie właściwego balansu pomiędzy automatyzacją testów na różnych poziomach w stosunku do czasu ich przeprowadzenia. Im szybszy feedback i przetestowanych przypadków brzegowych – tym lepiej. Chcemy natomiast testować aplikację nie tylko w izolacji, ale też w całości – wszystkie kluczowe ścieżki procesów, ale bez przypadków brzegowych.
Rodzaje testów
Aby wykorzystać zalety testów na poszczególnych poziomach i uniknąć ich wad, stosuje się rozróżnienie testów, aby komplementarnie się uzupełniały.
Testy jednostkowe
Sprawdzają mały fragmentu kodu (jednostkę), czyli klasę, metodę lub nawet zestaw klas z pakietu, tworzących jedną, ale wciąż niewielką funkcjonalność. Weryfikują działanie kodu pod względem logicznym, sprawdzają wiele warunków brzegowych.
Działają w izolacji, a zależności do innych funkcjonalności (interfejsów, klas) lub I/O zastępują mockami z pewnymi założeniami działania. Cały system nie musi być zintegrowany, aby zweryfikować działanie fragmentu aplikacji.
Są szybkie, ich wykonanie to kilka milisekund, dlatego stanowią podstawę piramidy testów. Modyfikując system, otrzymujemy informację zwrotną w krótkim czasie.
Przykład: kalkulator wysokości raty kredytu w zależności od różnych parametrów i warunków brzegowych takich jak kwota kredytu, okres trwania, rodzaj spłacanych rat.
Testy jednostkowe mają pewne wady:
- Nie dają gwarancji, że po skonfigurowaniu klas ze sobą, cały kod realizuje funkcjonalność poprawnie. Problem ten rozwiązują testy komponentowe.
- Nie wykrywają błędów w integracji z infrastrukturą. Stosujemy więc testy integracyjne.
Istnieje dyskusja czym jest „jednostka”. Należy pamiętać, że im mniejsze elementy testujemy, tym bardziej kosztowna jest refaktoryzacja, ponieważ jest więcej miejsc do zmiany. Testy ewoluują razem z kodem, bo zmienia się układ klas. Natomiast im większe – tym jest dłuższy czas wykonania.
Ja przyjmuję zasadę, że testuje publiczne metody API klas w miarę niezależnych modułów. Wówczas mogę pozwolić sobie na refaktoryzację wewnątrz modułu (prywatne pakietowe klasy, struktury), a publiczne API powinno zachować się tak samo. To rozsądny kompromis jednocześnie poprawiający design, bo prowadzi do niewielkiej liczby publicznych struktur i metod.
Testy komponentowe
Sprawdzają, czy zintegrowane komponenty (skonfigurowane pojedyncze klasy) realizują swoje funkcjonalności jako całość, ale w izolacji od innych komponentów. Sprawdzają przypadki użycia w wybranej ścieżce pozytywnej, negatywnych (z rozróżnieniem na powód niepowodzenia), ale niekoniecznie każde pojedyncze przypadki brzegowe, które są już przetestowane na poziomie szybko działających testów jednostkowych.
Są nadal szybkie, bo są odizolowane od otoczenia i nie łączą się do zewnętrznych źródeł (I/O, REST, baza danych, systemów kolejkowych). Otoczenie zastępują te implementacje Mockami, Stubami i implementacjami In-memory (bez konieczności zestawiania fizycznych połączeń, np. HSQLDB jako baza danych in-memory), np. rozpoczynając przetwarzanie od momentu otrzymania żądania po danym protokole. Izolują się od innych komponentów, np. zakładając, że dane są przekazywane z modułu do modułu.
Czym jest komponent to dyskusja, która powinna odbyć się w zespole. Można przyjąć, że pewien wycinek aplikacji realizujący pewien podzbiór wymagań jest komponentem (np. fragment aplikacji zamknięty w moduł gradle/maven). Jeżeli testujemy całą aplikację jako jeden komponent, są to raczej większe testy In-memory.
Przykład: kalkulowanie rabatu koszyka zakupowego w zależności od ustawionych polityk rabatowych na produkty oraz darmową dostawą w obrębie 10 km bez zważania na sposób implementacji – podział na pakiety/klasy.
Uwaga: w testach komponentowych powinniśmy używać konfiguracji produkcyjnych mówiących, jak poszczególne klasy są ze sobą złożone (a nie konfiguracji częściowych stworzonych na potrzeby testów). Mockujemy tylko infrastrukturę i ew inne moduły. Gdybyśmy posiadali osobną konfigurację dla testów i osobną produkcyjną, zwiększa się ryzyko raportu false-positive – zielonych testów i zepsutej produkcji.
Testy integracyjne
Testy integracyjne sprawdzają, czy interakcja pomiędzy naszą aplikacją i jej otoczeniem działa poprawnie. Mają na celu sprawdzić ewentualne błędy techniczne na warstwie protokołu, zestawu odpowiedzi. Błędy logiczne staramy się wyłapać w testach jednostkowych i komponentowych.
Na ich poziomie sprawdzana jest:
- Baza danych – czy zapytania działają poprawnie
- Komunikacja z innymi aplikacjami po HTTP/REST
- Komunikacja przez system kolejkowy
Do swojego działania potrzebują uruchomionego środowiska (lub jego części), w związku z tym mogą być niestabilne. Sugeruję skonfigurować ten zestaw testów jako nieblokujących wydanie artefaktu, aby nie zablokować sobie procesu wydawniczego oprogramowania (np. przez niestabilne chwilowo środowisko) i móc ocenić ręcznie ocenić stan integracji. Można je uruchomić osobnym profilem/jobem na CI lub w ramach budowania, ale z pluginem nieprzerywającym builda (np. fail safe dla mavena)
Mogą działać wolno, dlatego nie mamy wyniku od razu, ze względu na konieczność nawiązania połączenia, odpowiedzi systemu, od/do którego mamy zależność.
Istnieją techniki, dzięki którym możemy pominąć niestabilność środowiska, np.: możliwość uruchomienia jego części na żądanie albo Consumer-Driven Contract.
Testy systemowe/aplikacyjne
Testy systemowe (lub aplikacyjne) są uruchamiane obok działającej aplikacji i symulują do niej ruch, po czym sprawdzają obserwowalny, kwantyfikowany wynik jej działania. Przykładowo:
- Wysyłane są żądania HTTP i sprawdzany jest wynik odpowiedzi lub w innym miejscu (inny adres HTTP/baza danych)
- Wysyłane są zdarzenia na kolejkę i sprawdzany jest zmieniony stan systemu
Ich zadaniem jest sprawdzenie, czy aplikacja działa jako całość i realizuje swoje funkcje – przez dostępne protokoły, uruchomiona i podłączona pod swoje otoczenie systemy aplikacja.
Sprawdzamy główne ścieżki przepływu procesów biznesowych z perspektywy czarnej skrzynki (blackbox testing). Nie sprawdzamy natomiast wszystkich przypadków brzegowych. Dobrze jest sprawdzić: ścieżkę sukcesu, kilka ew. ścieżek porażki.
Testy mogą działać wolno oraz wymagają wdrożenia aplikacji na środowisko testowe.
Mogą być niestabilne jeżeli zależności aplikacji są niestabilne.
Testy e2e
Testy e2e (czyli end-to-end) to zestaw scenariuszy, które angażują rozpoczęcie procesu biznesowego w aplikacji, spowodowanie całego przepływu żądania przez różne systemy. Są to testy front-to-back, czyli tzw. od początku do końca. Ich celem jest sprawdzenie, czy poszczególne systemy poprawnie zrealizowały swoje zadanie, utrwaliły poprawne wartości i czy środowisko jako całość działa poprawnie. Ze względu na złożoność testów, najczęściej realizuje się je manualnie (można automatyzować).
Przykład: Klient zleca dyspozycję przelewu i otrzymuje komunikat potwierdzenia przyjęcia operacji do realizacji. Za chwilę przelew ląduje w systemie księgowym, a klient otrzymuje powiadomienie. Dodatkowo aktualizowana jest historia transakcji oraz stan konta. Sprawdzamy system księgowy, systemy pośrednie oraz UI użytkownika.
Efektywne uruchamianie testów w praktyce
Dobrze jest skonstruować proces budowania aplikacji w taki sposób, aby:
- Testy jednostkowe wszystkich modułów wykonały się jako pierwsze. Wtedy otrzymujemy szybki feedback z testów jednostkowych, nie czekając na wykonanie testów komponentowych.
- Następnie wykonały się testy komponentowe.
- Dalej uruchamiane są niezależnie testy integracyjne, ale ich błędy nie powodują zatrzymania procesu budowania. Możemy wtedy arbitralnie zdecydować się na wydanie aplikacji, pomijając tymczasowy problem środowiskowy.
- Deployment aplikacji na środowisko testowe i uruchomienie testów systemowych / aplikacyjnych / zautomatyzowanych e2e.
- Promocja na produkcję.
Inne rodzaje testów
Testy funkcjonalne – Czy system działa zgodnie z założeniami – Czy spełnia swoją funkcję?
- Wcześniej wymienione w artykule oraz,
- Testy eksploracyjne
Testy niefunkcjonalne – Czy system spełnia swoją funkcję w wystarczająco dobry sposób ze względu na inne wymagania?
- Testy wydajnościowe (performance tests)
- Jaki jest 95- i 99- percentyl oczekiwania na odpowiedź
- Ile pamięci proces potrzebuje do ukończenia zadania
- Jakie jest opóźnienie (latency) od żądania do uzyskania wyniku
- Testy obciążeniowe – działanie aplikacji pod wysokim obciążeniem przez dłuższy czas
- Czy istnieją wycieki pamięci
- Czy aplikacja się zawiesza, czy są długie pauzy GC
- Testy bezpieczeństwa
Wpis który czytasz to zaledwie fragment wiedzy zawartej w Programie szkoleniowym Java Developera od SoftwareSkill. Mamy do przekazania sporo usystematyzowanej wiedzy z zakresu kluczowych kompetencji i umiejętności Java Developera. Program składa się z kilku modułów w cotygodniowych dawkach wiedzy w formie video.
Zakończenie
Od poprawnie zbudowanego zestawu testów, odpowiedniej ich ilości na poszczególnych poziomach zależy nie tylko to, jak dokładnie przetestujemy oprogramowanie, ale również jak szybko zestaw testów poinformuje nas o wprowadzonym błędzie. Ma to znaczący wpływ na biznes i time-to-market.
Piramida testów pokazuje procentowo jaką ilość testów (mniej-więcej) powinniśmy mieć na danych poziomach. Testy jednostkowe są szybkie, więc za ich pomocą chcemy wykrywać jak najwięcej błędów. Z kolei nie gwarantują, że cała aplikacja działa poprawnie i jest dobrze skonfigurowana, dlatego istnieją testy komponentowe. Te zaś nie sprawdzą działania aplikacji w jej otoczeniu, stąd testy integracyjne i aplikacyjne.
Piguła wiedzy o najlepszych praktykach testowania w Java
Pobierz za darmo książkę 100 stron o technikach testowania w Java
Gdybyś potrzebował jeszcze więcej:
Jesteś Java Developerem?
Przejdź na wyższy poziom wiedzy
„Droga do Seniora” 🔥💪
Jesteś Team Leaderem? Masz zespół?
Podnieś efektywność i wiedzę swojego zespołu 👌
Szybkie podsumowanie
Piramida testów to zróżnicowanie ilości testów w zależności od ich typów. Im wykonanie testu jest tańsze, szybsze, bardziej stabilne – tym powinno ich być więcej i znajdują się na niższym szczeblu drabiny, stanowiąc jej fundament.
Testy można podzielić ze względu na ich izolację: jednostkowe, komponentowe, integracyjne, aplikacyjne/systemowe, e2e.