Programisto. Testuj!

Wszystko zaczyna się w momencie wytwarzania kodu, a dokładniej mówiąc w momencie rozwiązywania danego problemu za pomocą kodu. Czasem skomplikowane z początku sprawy okazują się być jednolinijkowym rozwiązaniem.

Świetnym przykładem jest ostatnio przeze mnie implementowana zmiana. Zadanie na które poświęciłem kilka godzin zakończyło się dosłownie – 38 znakami wprowadzonymi do kodu źródłowego.

Mały krok w kodzie. Duże zmiany dla aplikacji.

Niech natomiast nie zmyli Cię te jedyne 38 znaków. Nawet małe zmiany mogą wpływać w sposób krytyczny na działanie aplikacji. Przykładem może być np. sposób komunikacji z oprogramowaniem.

Jeśli głównym punktem dostępu do naszej aplikacji jest interfejs webowy, to zmiana wprowadzana na poziomie kodu odpowiedzialnego za komunikację przy pomocy HTTP może uniemożliwić prawidłową obsługę protokołu, np. zła obsługa żądania lub odpowiedzi. Finalnie aplikacja całościowo może przestać działać w sposób prawidłowy.

Zbytnia pewność siebie

Realizując czasem małe zmiany, szczególnie takie „improvements” poruszamy się w już istniejącym kodzie. Czasem na czuja zmieniamy układ kodu, czy logikę warunków. Myśląc, że nic złego się nie stanie.

Po zmianach w kodzie następuje kompilacja (u szczęściarzy) lub co najwyżej uruchomimy lintera, żeby sprawdzić code style oraz składnie. Natomiast brakuje nawyku. Nawyku testu regresji.

Po kilku udanych projektach (lub też częściej zadaniach) łatwo załapać przeświadczenie o posiadanych, dużych umiejętnościach. Nikt nie przyczepił się do kodu na Code Review, podczas testów nie pojawiły się żadne krytyczne błędy. Lepiej. Ktoś nas pochwalił na retro, że sprawnie dostarczyliśmy trudną funkcję do aplikacji.

Szybko i na c(z/h)uja

Dużo mówimy o szybkich zmianach i dostarczaniu wartości. Czasem pomijając jakość, bo czas staje się dla nas najważniejszym priorytetem. Wprowadzasz zmiany, na wyczucie, jeden warunek tu, drugi tam. Zmiana logiki, która do tej pory była uważana za jedyną słuszną.

Komponenty działające w zakresie swojego kontekstu mogą posiadać w pełni kompatybilne interfejsy i być w 100% sprawne. Natomiast gdy odwołujemy się dalej, wykorzystujemy kod spoza bazy danego komponentu, tworzymy zależności. Te zależności nie zawsze są łatwe w rozluźnianiu, czasem musimy radzić sobie z tym co mamy. Wystarczy wprowadzona zmiana w tej zależności bez weryfikacji uzależnionych od niej fragmentów. Więcej niż pewne, że pojawią się błędy.

Kto by weryfikował, że aplikacja się uruchomiła (o tym pisał ostatnio Mateusz w swoim artykule – Programisto. Weryfikuj zmiany!). A tym bardziej, przechodząc nawet najprostszą drogę, czyli Happy Path. Czy jest dalej możliwy do przejścia?

Niby mała zmiana nic nie zepsuje. Jednak ta mała zmiana wpływa na element w innej części systemu, który nie został zaktualizowany w sposób umożliwiający wykorzystanie nowego interfejsu. Ciężko zweryfikować ten fakt bez wykonania odpowiednich testów.

Krok po kroku

Jak w takim razie podchodzić do weryfikacji swoich zmian? Zrobiłem prostą listę rzeczy niezbędnych do sprawdzenia, przed puszczeniem swoich zmian w świat. Czyli dalej na Code Review, czy też na weryfikację przez QA.

To proste sześć kroków:

  • Uruchomienie narzędzi SCA.
  • Uruchomienie dostępnych testów automatycznych.
  • Kompilacja projektu.
  • Uruchomienie projektu.
  • Happy Path funkcji w oprogramowaniu, którą zmieniałeś / dodawałeś.
  • Weryfikacja najbliższych punktów integracji, które mogła naruszyć wprowadzona zmiana.

Być może w Twojej aplikacji jest więcej elementów, niezbędnych do weryfikacji po zmianach. Możesz przygotować checklistę (dla całego zespołu lub swoją), po której będziesz iterował za każdym razem tak, aby wyrobić w sobie nawyk.

Zauważ, że każdy z wynienionych kroków to testowanie danej warstwy aplikacji – standardów kodu, możliwości zbudowania, uruchomienia aplikacji, a następnie części funkcjonalnej.

Automatyzacja

Aby jednak praca nie przypominała cyklu, w którym przez większość czasu weryfikujemy swoje zmiany w sposób manualny, warto zastanowić się nad wdrożeniem elementu automatyzacji.

Nie każdy projekt musi podążać w zamyśle Continuous Integration/Delivery/Deployment, natomiast narzędzia towarzyszące tym konceptom bardzo łatwo wykorzystać do robienia brudnej roboty za nas.

Pierwsze cztery elementy z listy zawartej w poprzednim punkcie realizuję w projektach na zasadzie sprawdzania integralności kodu. Po każdym pushu na dowolnego brancha w repozytorium wykonywany jest zestaw odpowiednich kroków. Można powiedzieć, że to początek naszej drogi do dostarczania zmian.

W ostatnich projektach wykorzystywałem w tym celu GitLab CI, dzięki któremu w izolowanym, powtarzalnym środowisku (kontenery Docker) weryfikowane są wszystkie metryki związane z statyczną analizą kodu, uruchomieniem testów automatycznych, budowaniem aplikacji.

Dopiero gdy wszystkie te elementy zostaną wykonane z sukcesem, otrzymuję możliwość wdrożenia tych zmian na przygotowane środowisko testowe. To na nim jestem w stanie zweryfikować ostatnie dwa punkty. Dzieje się to za pomocą obrazów Docker oraz uruchomienia ich za pomocą Docker Compose. W szybki sposób można zweryfikować krytyczne punkty aplikacji. Chciałbym jeszcze dodać, że na tym poziomie, możemy uruchomić także zestaw Smoke Testów (być może zautomatyzowanych np. w Cypress – polecam).

Dzięki temu nie muszę realizować za każdym razem wszystkich niezbędnych kroków na środowisku lokalnym. Większość z nich odbywa się automatycznie. Oczywiście, nie jest to rozwiązanie idealne – raczej na poziomie wystarczająco dobre. Chcę jedynie pokazać, że procesy związane z automatyzacją nie wymagają skomplikowanej architektury, czy ogromu doświadczenia.

Dodatkowo zmusza nas do przemyślenia automatyzacji także w innych częściach naszej aplikacji, np. przy wersjonowaniu bazy danych. Po to, by przy każdym automatycznym wgraniu aplikacji na serwer testowy nie nanosić ręcznie skryptów, które modyfikują aktualną bazę danych do stanu wymaganego przez aktualnie wgraną wersję aplikacji.

Podsumowanie

Skupię się na trzech rzeczach, które powinieneś zapamiętać.

1. Zaufanie

Pisanie oprogramowania to trochę jak rozwiązywanie zadań z matematyki.

Na sprawdzianie rozwiązywałem wszystkie zadania, teoretycznie wyniki wyglądały dobrze. Stawiałem przynajmniej na otrzymanie 4+. Po odebraniu wyników ze sprawdzianu nagle się okazało, że wypadłem na ledwo naciągnięte 2.

Czemu?

Bo nie przetestowałem swojego rozwiązania. Zbyt zaufałem intuicji.

2. Weryfikacja

Każda wprowadzona zmiana powinna zostać przez Ciebie sprawdzona. Począwszy od uruchomienia narzędzi SCA, testów, kompilacji, po uruchomienie aplikacji, sprawdzenie Happy Path oraz najbliższych punktów integracji, na które mogła wpłynąć wprowadzona zmiana. Zastanów się nad Smoke Test, może zaoszczędzą Ci nie tylko czas, ale także dadzą trochę spokoju?

Oczywiście, nie zawsze wszystko jesteś w stanie dostrzec, czasem potrzeba głębszych testów. Jednak po wymienionych elementach wcześniej jesteś o kilka kroków do przodu przed tymi, którzy ich nie stosują.

3. Automatyzacja

Lubię oszczędzać czas, szczególnie gdy muszę realizować rzeczy za którymi szczególnie nie przepadam (lepiej, mogę napisać kod – co lubię). Ciągle manualne testy? Mogą wzbudzać frustrację. Tym bardziej przy złożonych procesach.

Jeśli powtarzasz manualne kroki po raz kolejny, czas zastanowić się nad ich automatyzacją. Wykorzystując do tego narzędzia wymagające minimalnego nakładu pracy, ale dające maksimum efektu.

A co zrobić z czasem, który udało się nam zaoszczędzić? Przeznaczyć na rzeczy, które lubisz w swojej pracy np. pisanie kodu. Kodu testów automatycznych i dostarczanej implementacji.

Przygotowane testy automatyczne są tym czego potrzebujemy do długofalowego rozwijania aplikacji. Dobre testy stają się nieocenioną pomocą, ułatwiającą wprowadzanie zmian. Jeśli zmienimy coś w sposób nieprawidłowy – szybko otrzymamy stosowną informację.