Trenerzy info mobile

Kod testujący Twój kod, czyli o automatycznych testach oprogramowania

A co gdybym Ci powiedział, że napisanie działającego kodu to dopiero połowa sukcesu? Kod źródłowy jest oczywiście najważniejszy i to dzięki niemu Twoja aplikacja zrealizuje powierzone zadania. Jednak aby upewnić się, że działa on zgodnie z założeniami, warto poświęcić chwilę testom automatycznym.

 

Zadaniem testów automatycznych jest weryfikowanie poprawności działania kodu źródłowego w sposób zautomatyzowany i powtarzalny. Zwykle jest to fragment kodu korzystający z fragmentów kodu źródłowego lub całego programu, który weryfikuje, czy te w określonych warunkach zachowują się zgodnie z oczekiwaniami.  

 

Spis treści


  1. Czym są testy automatyczne oprogramowania i dlaczego warto je pisać?
  2. Rodzaje automatycznych testów oprogramowania
  3. Konstrukcja testu i przykładowy test
  4. W ramach podsumowania – rada eksperta dla początkujących testerów oprogramowania

 

 

Czym są testy automatyczne oprogramowania i dlaczego warto je pisać? 

Oczywiście przygotowanie zestawu testów automatycznych rodzi konieczność poświęcenia czasu na przygotowanie środowiska testowego oraz zaprojektowanie i napisanie samych testów. Inwestycja ta jednak zwraca się bardzo szybko i z dużą nawiązką. Z posiadania dobrze zaprojektowanego zestawu testów automatycznych wynika szereg zalet:
 

  • Zwiększamy szansę na to, że nasz kod działa poprawnie. Celowo napisałem “zwiększamy szansę”, ponieważ sam fakt posiadania zestawu testów nie oznacza jeszcze, że kod działa poprawnie. Kod testów, podobnie jak kod źródłowy, może zawierać błędy. Błędy mogą być ukryte w logice testu, w danych wejściowych, a także w założeniach poczynionych w trakcie projektowania testu. Istnieje również szansa, że nie wszystkie możliwości w kodzie zostały przetestowane i np. dla jakiegoś przypadku brzegowego (edge case) brakuje testu. 
  • Posiadanie testów to istotna oszczędność czasu. Wyobraź sobie, że przy każdorazowej zmianie w aplikacji musisz “przeklikać” lub ręcznie przetestować wszystkie scenariusze. Nie mam tu na myśli tylko miejsca, gdzie zostały dodane zmiany, ale wszystkie miejsca, których dodane zmiany mogą dotyczyć. Warto mieć na uwadze, że nie zawsze jesteśmy ich świadomi. Nawet jeśli dodane zmiany wpływają na bardzo mały wycinek aplikacji, to uruchomienie testu automatycznego zwykle będzie o wiele szybsze niż ręczna weryfikacja. A ponieważ czas to pieniądz, to ten poświęcony na manualne testowanie można spożytkować na coś innego, np. usprawnienie działania aplikacji lub dodanie nowych funkcji. Bezpieczeństwo wprowadzania zmian w kodzie jest przydatne w szczególności w trakcie refaktoryzacji. 
  • Testy to gotowa dokumentacja powtarzalnych scenariuszy, które pozwalają określić, jak powinna zachować się aplikacja w określonych warunkach. O ile daleki jestem od podpisania się pod stwierdzeniem, że “najlepszą dokumentacją kodu są testy”, tak z czystym sumieniem mogę stwierdzić, że testy są bardzo dobrym uzupełnieniem dokumentacji. Testy pozwalają zobaczyć, jakie intencje miał programista/programistka tworzący dany fragment kodu, zobaczyć w praktyce jego użycie i poznać przypadki brzegowe, które na pierwszy rzut oka mogłyby być niewidoczne. 
  • Oprócz udokumentowania, jak z danego komponentu lub programu korzystać, testy umożliwiają przetestowanie użyteczności publicznych interfejsów. Przykładowo dzięki testom możliwa jest weryfikacja, czy zaproponowany zestaw metod dla danego komponentu i ich parametrów ma sens, a ich wykorzystanie jest proste i wygodne. Po przeprowadzeniu testów może okazać się, że wybrane metody lub parametry można usunąć, uprościć lub dodać brakujące. 
  • Testy automatyczne pozwalają też na wygodne debuggowanie kodu źródłowego. Nie zawsze konieczne jest uruchomienie całej aplikacji. Często wystarczy uruchomienie wybranego fragmentu za pomocą testów z pożądanymi danymi wejściowymi, by znaleźć źródło problemu i jego rozwiązanie. 

 

 

Rodzaje automatycznych testów oprogramowania

Klasyfikacji testów można dokonać na kilku płaszczyznach. Najczęściej można spotkać podział testów w zależności od zasięgu testowanego kodu oraz ich celu. 

 

Dzieląc testy w zależności od zasięgu kodu , najczęściej spotykany podział to podział na:

  • testy jednostkowe,
  • testy integracyjne,
  • test end to end (E2E). 

 

Testy jednostkowe (unit tests) sprawdzają tylko unit, czyli mały fragment kodu. Często będą to metody lub funkcje, ale może to być też funkcjonalność w formie kodu wraz z wybranymi zależnościami (moduł). Testy jednostkowe powinny testować kod zawarty w samym unicie. Wyróżnia się dwa podejścia do testowania jednostkowego: 

  • szkoła londyńska – w tym podejściu zależności klasy zastępowane są zaślepkami (mocks), a testy skupiają się na przetestowaniu implementacji. Zaślepki mogą mieć zdefiniowane zachowanie, jeśli testowany kod oczekuje zwrócenia przez zależność określonego wyniku. Przykładowo, jeśli chcemy przetestować komponent obliczający wartość zamówienia, który wykorzystuje jako zależność przelicznik walut, to ów przelicznik i jego zachowanie powinny zostać zmockowane. 
  • szkoła Detroit – nie skupia się na wewnętrznej implementacji, a jedynie na wyniku. W szkole Detroit testy sprawdzają zachowanie całego modułu, a zmiana w którymś z modułów może skutkować niepowodzeniem testów. W przedstawionym wcześniej przykładzie w szkole Detroit można wykorzystać rzeczywistą implementację przelicznika walut. 

 

Każde z podejść ma wady i zalety oraz ma swoich zwolenników i przeciwników. Zachęcam, by wypróbować oba i samemu wybrać to, które pasuje nam bardziej. 

 

Z tematem testów jednostkowych wiąże się również akronim FIRST.

 

Reguły FIRST określają wytyczne, które powinny spełniać dobrze zaprojektowane testy jednostkowe: 

  • Fast – testy jednostkowe powinny być szybki, gdyż zwykle będzie ich dużo. Ponadto wolniejsze testy sprawiają, że są rzadziej uruchamiane. 
  • Isolated – test nie powinien zależeć od innego testu. Oznacza to również, że jeden test nie powinien mieć wpływu na inne. Niedopuszczalna jest sytuacja, gdy jeden test jest wymagany do pomyślnego przejścia innego.
  • Repeatable – testy powinny być powtarzalne tzn. dawać deterministyczne rezultaty. 
  • Self-validating – test powinien jednoznacznie informować, czy zakończył się pomyślnie czy nie. Innymi słowy, każdy test powinien zawierać co najmniej jedną asercję. 
  • Timely/Thorough – test powinien być możliwy do napisania w dowolnym momencie, zarówno przed powstaniem kodu źródłowego, jak i po jego napisaniu. 

 

Same testy jednostkowe to zdecydowanie za mało. .To,że dwa moduły działają w izolacji, nie znaczy, że zadziałają po połączeniu ich ze sobą. Tutaj na scenę wkraczają testy integracyjne. Jak sama nazwa wskazuje, ich zadaniem jest sprawdzenie, jak zachowują się zintegrowane ze sobą kawałki kodu. Istotę testów integracyjnych dobrze tłumaczą przykłady dostępne pod frazą “2 unit tests 0 integration tests”. 

 

Testy end to end (E2E) symulują rzeczywiste scenariusze użytkowania aplikacji lub systemu. Ich celem jest sprawdzenie poprawności działania aplikacji jako całości, od początku do końca, z perspektywy użytkownika końcowego. Testy E2E symulują typowe ścieżki, którymi użytkownicy mogą podążać podczas korzystania z aplikacji.

 

Co więcej, sprawdzają, czy wszystkie elementy i moduły aplikacji są poprawnie zintegrowane. Może to być testowanie np. połączenia aplikacji frontendowej z backendem, uruchomionym z prawdziwymi bazami danych, integracjami z zewnętrznymi systemami i usługami. Do testowania E2E aplikacji webowych możesz wykorzystać takie narzędzia jak Selenium, Cypress, Nightwatch.js czy Puppeteer. 

 

Jeśli chodzi o podział testów ze względu na ich przeznaczenie, to kategorii można wyróżnić znacznie więcej: 

  • testy akceptacyjne – sprawdzają, czy aplikacja spełnia kryteria akceptacji klienta lub użytkownika końcowego. 
  • testy funkcjonalne – sprawdzają, czy funkcje testowanego programu działają zgodnie z oczekiwaniami. 
  • testy regresji – sprawdzają, czy zmiany w kodzie aplikacji nie wpłynęły negatywnie na istniejące funkcjonalności. 
  • testy bezpieczeństwa – sprawdzają aplikację pod kątem odporności na powszechnie wykorzystywane ataki i znane zagrożenia.  
  • testy wydajnościowe – sprawdzają aplikację lub jej komponenty pod kątem wydajności. 
  • testy obciążeniowe – sprawdzają, jak aplikacja zachowuje się przy dużym obciążeniu. 

 

 

Konstrukcja testu automatycznego i przykład

Konstrukcja testów najczęściej wpasowuje się w podejście AAA Arrange, Act, Assert, dzielące test na trzy części. 

 

  1. Arrange – w tej części powinny zostać zainicjalizowane wszystkie komponenty oraz wartości niezbędne do przeprowadzenia testu. 
  2. Act – ta część odpowiada za przeprowadzenie testu tzn. wywołanie odpowiednich metod. 
  3. Assert – tutaj następuje weryfikacja założeń i wywołanie asercji, czyli mechanizmu sprawdzającego, czy rezultat testu jest zgodny z oczekiwanym. 

 

Nieodłączną częścią testu jest również jego opis. Opis powinien odpowiadać na pytanie, co konkretnie sprawdza dany test. Koniecznie musi być jednoznaczny.  

 

Zobaczmy, jak to podejście wygląda w praktyce. Na potrzeby przykładu przygotowałem prosty kod w TypeScripcie (link do wersji interaktywnej tutaj), którego zadaniem jest obliczenie wartości brutto zamówienia. 


 

Zadaniem kalkulatora jest policzenie wartości zamówienia. Metoda calculateOrderValue przyjmuje jako parametr obiekt zawierający wartość netto zamówienia oraz wartość liczbową zniżki. Zewnętrzną zależnością jest funkcja obliczająca wysokość podatku. Poniżej znajdziesz zestaw testów weryfikujących poprawność poszczególnych fragmentów kodu. Obecnie na rynku do testów kodu JavaScript wykorzystuje się wiele rozwiązań, np. Mocha, Jest, Vite, AVA i wiele innych. Nie chcę Ci sugerować żadnego z nich, dlatego też zdecydowałem się nie trzymać żadnego konkretnego, a same testy stanowią raczej formę pseudokodu.

 

 

Dla przedstawionego zestawu testów sekcją Arrange jest metoda beforeEach, która przed uruchomieniem każdego testu, do jego kontekstu przekazuje wartość podatku i wartość zamówienia netto oraz tworzy instancję klasy OrderCalculator z mockiem funkcji obliczającej podatek. W przypadku testów sprawdzających wariant zamówienia ze zniżką sekcja Arrange ustawia również wartość zmiennej discount. Natomiast sekcja Act to wywołanie metody calculateOrderValue. Natomiast sekcja Assert to sprawdzenie, czy wywołanie metody calculateOrderValue doprowadziło do oczekiwanych rezultatów. W dwóch testach będzie to obliczenie wartości zamówienia, a w pozostałych dwóch sprawdzenie, czy wyrzucony został oczekiwany błąd. 

 

 

W ramach podsumowania – rada eksperta dla początkujących testerów

 Testy automatyczne to temat na długie godziny. W końcu powstały o nich liczne publikacje, książki, a nawet dedykowane im szkolenia.

 

Na początek jednak zachęcam Cię do napisania pierwszego zestawu testów i doświadczenia korzyści z nich płynących na własną rękę! 

 

 


Serdecznie dziękujemy za kolejną publikację Dominika na Rock’n’Blog!

Dominik — You Rock 🤘 Razem #Wdrażamy Wiedzę

Zapraszamy na bloga Dominika: https://devszczepaniak.pl