Każdy programista, prędzej czy później, zaczyna pisać testy. W większości, wszyscy zaczynają od testów jednostkowych, choć zdarzają się również osoby, które przygodę z testami zaczynają od testów wysokiego poziomu, np. pisanych w behacie. Niestety zdarza się, że programiści pozostają przy tym, czego się nauczyli na początku, ignorując wachlarz zalet pełnego zakresu testowania. Dlatego od dziś, w kilku postach, poruszę temat testów deweloperskich.

Zacznijmy od początku

Mówiąc najprościej, jak się da, testami nazywamy mikro-programy, które korzystając z kodu naszej aplikacji (bądź z samej aplikacji) sprawdzają, czy napisany przez nas kod spełnia wymagania, które w tych testach sprecyzowaliśmy. Do tworzenia testów korzysta się z wyspecjalizowanych bibliotek. W języku PHP wiądącymi bibliotekami są: PHPSpec, PHPUnit oraz Behat.

Jak to na świecie bywa, jest wiele teorii na temat tego, jak dobrze pisać testy. Część programistów uważa, że powinniśmy testować każdy element aplikacji, nawet ten najprostszy. Kolejna grupa stwierdzi, że najlepszą metodą na pisanie testów jest stosowanie metodyki Test Driven Development, która polega na stosowaniu w pętli trzech etapów:

  • stworzenia kompleksowych scenariuszy testowych
  • implementacja funkcjonalności do momentu zapalenia się wszystkich testów na zielono
  • refaktoryzacja kodu, poprawki wydajnościowe oraz temu podobne

Jeszcze kolejna grupa programistów uważa, że testy powinna pisać osoba specjalnie do tego wyszkolona, czyli Tester Automatyczny. Jest jedna rzecz, która łączy wszystkie te grupy – każda z nich ma rację. Ja w swojej karierze przeszedłem przez wszystkie te „etapy”, najczęściej po oglądnięciu tej jedynej prezki, która miała odmienić mnie jako programistę.

Po kilku latach spędzonych na pisaniu testów mogę stwierdzić, że w zależności od sytuacji, jedno rozwiązanie może być lepsze od drugiego, bądź na odwrót. Najczęściej na sposób tworzenia testów składają się takie czynniki jak:

  • wiedza programistów na temat sztuki pisania testów
  • celów postawionych przez lidera zespołu deweloperskiego
  • niestety, warunki biznesowe, w jakich projekt został sprzedany
  • błędów osób decyzyjnych wynikających z braku odpowiednich kompetencji

Najlepiej, aby decyzje odnośnie sposobu pisania testów zostały podjęte już na starcie projektu. O ile zespół nie znajdzie lepszej metodyki, każdy z deweloperów powininen pisać testy w oparciu o panujące w projekcie reguły.

Charakterystyki testów pisanych przez dewelopera

Istnieje mnóstwo różnych rodzajów testów, które można napisać, aby móc zweryfikować, czy aplikacja działa zgodnie z założeniami. Dla mnie, programisty aplikacji biznesowych, najważniejszym jest dostarczyć aplikację, która spełnia wymagania dostarczone przez biznes. Zatem, najważniejsze dla mnie jest zweryfikowanie, czy wymagania te zostały spełnione. Do tego celu służą mi jedynie cztery rodzaje testów, których charakterystyki przedstawiam poniżej.

Testy jednostkowe

Najbardziej podstawowymi testami są testy jednostkowe. Są one bardzo mocno zintegrowane z naszym kodem. Ich zadaniem jest weryfikacja poprawnego działania jednostki kodu. Jako jednostkę najczęściej wybieramy metodę, choć nic nie stoi na przeszkodzie, aby była to jedna klasa, bądź nawet zestaw klas.

Najistotniejszą charakterystyką testów jednostkowych jest to, że uruchamiają one kod w całkowitym odseparowaniu od świata zewnętrznego. Jeżeli testujemy coś jednostkowo, to nie pozwalamy na komunikację kodu z systemem plików, bazami danych oraz innymi systemami zewnętrznymi. Dzięki temu, testy jednostkowe pracują jedynie w oparciu o pamięć oraz procesor, co czynie je ultra szybkimi.

Testy jednostkowe do swojej pracy nie potrzebują ustalonego wcześniej stanu systemu, ponieważ do ich poprawnej pracy nie potrzebujemy, aby nasza aplikacja (ani żaden jej fragment) była uruchomiona. Jedyne, o co musimy w tej przestrzeni zadbać, to odpowiednio dobrane wartości przekazywane do testowanych metod.

Najpopularniejszymi bibliotekami do tworzenia testów jednostkowych są PHPSpec oraz PHPUnit.

Testy integracyjne

Testy integracyjne, podobnie do testów jednostkowych, działają na przestrzeni kodu. Oznacza to, że nie potrzebują one działającej instancji aplikacji do poprawnego działania.

Główną różnicą pomiędzy testami jednostkowymi oraz integracyjnymi jest nic innego, jak integracja kodu ze światem zewnętrznym. Będziemy mieli więc tutaj możliwość kontaktu z systemem plików, serwisami bazodanowymi itp. Najczęściej, przed uruchomieniem pojedynczego testu, będziemy potrzebowali wygenerować tzw. fixturki, czyli zestaw danych początkowych znajdujących się w systemach, z którymi będziemy się kontaktowali w trakcie wykonania testu.

Ze względu na konieczność komunikacji z zasobami zewnętrznymi, testy integracyjne są wolniejsze w działaniu od testów jednostkowych.

Do tworzenia testów integracyjnych wykorzystujemy bibliotekę PHPUnit.

Testy funkcjonalne

Od poziomu testów funkcjonalnych przestajemy mówić o testowaniu kodu. Zamiast tego, zajmujemy się testowaniem aplikacji. Oznacza to, że w trakcie działania testu, potrzebujemy uruchomionej instancji naszej aplikacji*.

Testem funkcjonalnym nazywamy jednostkę testową, która weryfikuje działanie pojedynczego, tak prostego (w ramach funkcjonalności) scenariusza, jak to jest możliwe. Funkcjonalnie możemy przetestować takie rzeczy jak np. działanie formularza rejestracyjnego, logowanie, dodanie produktu do koszyka, wejście na stronę zasobu, do którego nie mamy dostępu. Pamiętajmy, że oprócz testowania aplikacji w formie klasycznej, funkcjonalnie możemy również testować wystawione wcześniej API.

Jest szkoła mówiąca, że weryfikacja działania aplikacji również powinna odbyć się w sposób funkcjonalny, tzn. jeżeli przykładowo dodamy nowy zasób przez API, to, w celu weryfikacji poprawnego dodania tego zasobu, powinniśmy odpytać endpoint zwracający informacje o tym zasobie. Moim zdaniem, jest to znacznie ograniczająca restrykcja, gdyż nie zawsze musimy posiadać endpoint zwracający informacje o zasobach.

Jako, że testy funkcjonalne potrzebują działającego fragmentu aplikacji, na samym początku testu, jesteśmy zobligowani do utworzenia fixturek, które spowodują, że ów fragment aplikacji będzie działał poprawnie.

Ze względu na pracę na działającej aplikacji, testy funkcjonalne są najczęściej znacznie wolniejsze w działaniu od testów integracyjnych.

Do tworzenia testów funkcjonalnych wykorzystujemy biblioteki PHPUnit oraz Behat.

Testy end-to-end

Test end-to-end w zasadzie są w mocno zbliżone do testów funkcjonalnych. Testują one wybrany fragment aplikacji. Testami end-to-end możemy objąć zarówno klasyczną aplikację, jak i opublikowane w obrębie aplikacji API.

To, co różni testy funkcjonalne od end-to-end, to zasięg scenariusza testowego. O ile funkcjonalnie testujemy jeden prosty scenariusz, o tyle testem end-to-end możemy przetestować konkretny przebieg procesu wewnątrz aplikacji. Zatem, testem end-to-end (w jednym scenariuszy) możemy objąć pełen proces rejestracji, weryfikowania oraz logowania użytkownika. Możemy również przetestować proces zakupowy od momentu włożenia produktu do koszyka, aż do momentu, w którym ujrzymy stronę podziękowania za złożenie zamówienia. Możemy również przetestować proces po stronie API, wywołując konkretne endpointy jeden za drugim.

Ze względu na fakt, że działamy szerszym zakresie w obrębie aplikacji, może tutaj istnieć konieczność przygotowywania znacznie bardziej rozległych fixturek. Dodatkowo, zanim uruchomimy test, możemy potrzebować zainicjować pewne procesy wewnątrz aplikacji np. zaindeksować przygotowane w fixturkach dane do indeksu ElasticSearcha. To wszystko powoduje, że niewątpliwie, testy end-to-end są jednymi z najwolniejszych testów.

Do tworzenia testów end-to-end będziemy używali bibliotek PHPUnit oraz Behat.

Piramida testowania

Celowo, każda z powyższych charakterystyk zawiera informację na temat szybkości działania testów. Niestety, im wyższy poziom testowania, tym dłużej trwa wykonanie pojedynczego testu. O ile kilka tysięcy testów jednostkowych powinno wykonać się w przeciągu jednej-dwóch sekund, to jeden test end-to-end potrafi wykonywać się w nawet kilkanaście sekund.

Powyższe oznacza, że raczej słabym pomysłem będzie bardzo dokładne pokrycie aplikacji testami end-to-end, bo czas wykonania testów będzie na tyle duży, że programista straci zainteresowanie w ich uruchamianiu. Zgodnie ze zdrowym rozsądkiem powinniśmy rozłożyć testy w sposób zbliżony do Piramidy Testowania:

Piramida Testowania przedstawia optymalny (względem czasu wykonania) rozkład testów, który powinniśmy utrzymywać w tworzonej przez nas aplikacji biznesowej. Samo utrzymywanie tego rozkładu nie jest łatwym zadaniem. Jest jednak zasada, która powinna nam w tym pomóc. Polega ona na tym, że jeżeli moglibyśmy konkretne wymaganie przetestować na kilku różnych poziomach (jednostkowy, integracyjny itd.), to zawsze wybierajmy niższy poziom testów.

Zasada 3xA

Każdy z wymienionych przeze mnie rodzajów testów różni się czymś od pozostałych. Jest jednak coś, co łączy każdy z nich – jest to zasada 3xA, czyli Arrange, Act oraz Assert.

Zasada ta mówi, że każdy test powinien składać się z trzech, następujących po sobie, sekcji:

  • Arrange – jest to część testu służąca wszelkim przygotowaniom, które należy przeprowadzić przed testem. Przykładowo, czyścimy tutaj zbędnie wygenerowane przez poprzednie uruchomienie dane, generujemy również fixturki potrzebne do następnego uruchomienia.
  • Act – jest to część testu, w której uruchamiany jest testowany fragment kodu lub aplikacji. Bardzo często kojarzy się to z wywołaniem pojedynczej metody. Nic nie stoi jednak na przeszkodzie, aby była to seria wywołań metod czy żądań HTTP.
  • Assert – jest częścią służącą do weryfikacji zwróconych przez sekcję Act danych. Ze względu na to, że bardzo lubimy pisać assercje, jest to zazwyczaj najobszerniejsza część testu 🙂

Dobrą praktyką jest, aby patrząc na test, można było łatwo określić granice pomiędzy każdą z wymienionych wyżej sekcji.

Moje przemyślenia na temat testowania

Kilka lat spędzonych na pisaniu testów pozwoliło mi wyrobić sobie zdanie na kilka bardzo popularnych kwestii, które co jakiś czas pojawiają się w dyskusjach. Jeżeli masz inne zdanie na którykolwiek z poniższych punktów – nie ma sprawy 🙂 Jeżeli jednak jesteś na etapie poszukiwań – zachęcam do zapoznania się z moimi przemyśleniami.

Testy automatyczne nigdy nie zastąpią testów akceptacyjnych

Testami akceptacyjnymi nazywamy testy manualne, które przeprowadzane są po stronie podmiotu odbierającego aplikację od zespołu developerskiego. Mają one postawiony inny cel niż testy developerskie. Dlatego właśnie nie powinniśmy z nich rezygnować. Pamiętajmy, że wprawne oko testera (lub wymagające oko klienta) mogą zwrócić uwagę na rzeczy, których za nic nie wyłapiemy w testach developerskich.

Testy warsztatem zespołu, nie wolą klienta

Kiedy tworzymy oprogramowanie na zamówienie, bierzemy odpowiedzialność za jego poprawne działanie. To my musimy mieć pewność, że wszystko jest na tip-top. Do tego celu właśnie piszemy testy, co ma niestety swoje odzwierciedlenie w estymacjach zadań. Nie powinniśmy jednak ulegać jakiejkolwiek presji ze strony osoby zamawiającej, abyśmy zrezygnowali z tworzenia testów, ponieważ „skróci to czas realizacji zadania”. Krótkofalowo, faktycznie – klient będzie widział mniejszy rachunek (oczywiście tylko na początku prac). Jeżeli jednak spojrzymy na tą kwestię długofalowo, to dzięki testom odpadną nam trudne w odtworzeniu błędy, znajdziemy wzajemnie sprzeczne wymagania biznesowe, oraz co najważniejsze – o bugach będzie informować nas test, a nie klient.

Ty decydujesz, co testujesz mocniej, a co słabiej

Testy nie powinny być dla nas smutnym obowiązkiem. Powinniśmy traktować je bardziej jako narzędzie, którego użycie w taki lub inny sposób może przynieść nam wartość. W trakcie developmentu to my wiemy, gdzie i co da nam wymierne korzyści. Bezrefleksyjne uleganie jakiejkolwiek modzie bądź schematom związanym z tym, co i jak testujemy – zazwyczaj się nie spłaca. Bardzo często może to popychać nas w zjawisko tzw. betonowania się testami.

Dlaczego nie lubię 100% coverage?

Przykład osągania 100% pokrycia kodu testami jest wzorcowym przykładem z poprzedniego punktu. Pokrycie kodu testami jest fajną statystyką, która może pokazać nam na przestrzeni czasu, czy trzymamy dobry kurs. Korzystanie z tej statystyki w sposób mało prawidłowy, czyli próbę osiągnięcia 100% pokrycia np. testami jednostkowymi może skutkować takimi rzeczami, jak specki zapytań napisanych w Doctrine Query Builderze.

Dobrze napisane scenariusze to podstawa

Jedną z najważniejszych dla mnie rzeczy, związanych z pisaniem testów, jest przygotowywanie odpowiedniego zestawu scenariuszy testowych. Jeżeli nie potrafię dobrze nazwać któregokolwiek z dostrzeżonych przeze mnie scenariuszy, to znaczy, że testowany kod wymaga jeszcze nieco pracy. Najczęściej może to oznaczać, że potrzeba powydzielać gdzieniegdzie kod do osobnych klas.

*czasami jest to realizowane poprzez uruchomienie kernelu aplikacyjnego w trakcie trwania testu.

Profesjonalista, zajmujący się na co dzień aplikacjami biznesowymi w ekosystemie PHP. Jego pasją jest odkrywanie nowych konceptów programistycznych oraz wzorce architektoniczne. Uwielbia również pisać testy, gdyż jak sam uważa, dobry kod to przetestowany kod.

Comments are closed.