Przejdź do treści

Ćwierć wieku testowania ręcznego – ile naprawdę kosztuje klik, klik, działa

Znowu zacznę od wyznania, bo taka forma najlepiej mi wychodzi. Ale najpierw sprostuję własny tytuł, żeby nie było nieporozumień: nie przejechałem ćwierć wieku bez testów automatycznych. Piszę je od dawna, a testowaniem zająłem się na tyle serio, że skończyłem z niego studia podyplomowe. Ten wpis jest o czymś innym – o pierwszym odruchu. Bo przez ponad ćwierć wieku pierwszy ruch po napisaniu kodu miałem zawsze ten sam: klikałem. Napisałem funkcję – klikałem. Poprawiłem formularz – klikałem. Działa? Działa. Wdrażam. Od skryptów w cgi-bin przez długie lata PHP aż głęboko w epokę frameworków to ręczny test był domyślną odpowiedzią na pytanie „czy to działa” – a wszystko inne bywało dodatkiem do niego, nie odwrotnie.

I chcę od razu powiedzieć: to nie było lenistwo. Na początku nie było sensownej alternatywy – kultura testów automatycznych w webie praktycznie nie istniała, a ręczne przeklikanie tego, co się właśnie napisało, było po prostu tym, co robili wszyscy. Problem w tym, że nawyk został ze mną o wiele dłużej niż powody, dla których powstał. Dokładnie ten sam mechanizm, o którym pisałem przy jQuery: coś było kiedyś rozsądkiem, a potem niepostrzeżenie stało się tylko przyzwyczajeniem. Epoki, które opiszę niżej, nie mają zresztą ostrych granic – nakładały się latami. Piszę o tym, co w danej epoce było u mnie domyślne, nie jedyne.

Dlaczego ręczny test zawsze oszukuje tak samo

Ręczne testowanie ma jedną wadę, która nie zależy od staranności: sprawdzasz to, co właśnie zmieniłeś – a psujesz to, o czym w tej chwili nie myślisz. Poprawiłem zapytanie w jednym widoku, przeklikałem ten widok, wszystko gra. Tego, że ta sama funkcja była używana w trzech innych miejscach i jedno z nich właśnie przestało działać, ręczny test nie pokaże, bo tam nie kliknąłem. To się nazywa regresja i to ona, nie świeże błędy, była przez lata moim najdroższym problemem.

Świeży błąd jest tani: piszesz, klikasz, nie działa, poprawiasz – wszystko w ciągu minut, z całym kontekstem w głowie. Regresja jest droga, bo wypływa po czasie, w miejscu, którego nie ruszałeś, i bez żadnego związku z tym, co masz akurat w głowie.

Trzy sposoby, w jakie dowiadywałem się o błędach

Skoro nie mówiły mi o nich testy, mówiło mi o nich życie. W trzech wariantach, uszeregowanych od najlepszego do najgorszego.

Wariant pierwszy: log z błędem 500. To był luksus, choć wtedy tak o tym nie myślałem. Błąd, który kończy się pięćsetką, przynajmniej krzyczy – zostawia ślad w logu, często ze stosem wywołań, i da się go znaleźć od ręki. Ale pięćsetka łapie tylko błędy, które wywracają wykonanie: literówkę w nazwie, wywołanie na null, złe zapytanie. Błędy logiczne nie rzucają pięćsetek. Program, który liczy źle, ale liczy, wygląda w logach dokładnie tak samo jak program, który liczy dobrze.

Wariant drugi: zgłoszenie od użytkownika. Ktoś napisał, że coś nie działa. Z jednej strony dobrze – błąd znaleziony. Z drugiej: znalazł go ktoś, kto nie podpisywał się pod tym kodem, i zwykle znalazł go w najmniej wygodnym momencie. Do tego zgłoszenie od użytkownika to opis objawów, nie diagnoza – między „nie mogę zapisać formularza” a znalezieniem przyczyny potrafiła minąć godzina samego odtwarzania problemu.

Wariant trzeci, najgorszy: nikt nie zgłosił niczego. Zdarzyło mi się odkryć – przypadkiem, przy okazji czegoś innego – że funkcja serwisu nie działała od kilku dni. Żadnego błędu w logach, żadnego zgłoszenia. Użytkownicy po prostu trafiali na coś zepsutego, wzruszali ramionami i wychodzili. Naprawiłem sam, po cichu, ale to uczucie pamiętam do dziś: nie wiedziałem, ilu osobom się to przytrafiło, bo cisza w logach przestała cokolwiek znaczyć. Cisza to nie jest sygnał, że wszystko działa. Cisza to brak sygnału.

Epoka druga: testy pisane po kodzie

W końcu zacząłem pisać testy automatyczne – tak jak chyba większość: po kodzie. Najpierw funkcja, potem – jeśli starczyło zapału – test do niej. Na blogu został ślad tamtej epoki: wpis o testowaniu aplikacji CRUD w Symfony, pisany dokładnie w tym trybie.

I tu ważna rzecz: to nie była kwestia braku wiedzy. Z testowania oprogramowania skończyłem studia podyplomowe – techniki, poziomy testów, procesy znałem także od strony formalnej. A pierwszym odruchem po napisaniu własnej funkcji i tak bywał klik. To jest chyba najuczciwszy dowód tezy, która przewija się przez tego bloga: sama wiedza nie łamie nawyku – co najwyżej funduje mu wyrzuty sumienia.

Testy po kodzie to ogromny skok względem klikania – regresje wreszcie zaczęły wychodzić na jaw przed wdrożeniem, nie po. Ale ten tryb ma wbudowaną słabość psychologiczną: test do działającego kodu jest pracą dodatkową. Kod już działa – sam sprawdziłem, przecież klikałem – więc test wydaje się formalnością, którą można dopisać jutro. A jutro jest nowe zadanie. Tak powstawały luki: nie w miejscach trudnych, tylko w miejscach, na które nie starczyło chęci.

I te luki łatałem w najgorszym możliwym trybie: po awarii. Coś się wywalało na produkcji, naprawiałem i wtedy – już bez dyskusji – dopisywałem test na ten przypadek. Taki test jest jak blizna: świadczy o tym, że rana była prawdziwa, ale powstał o jedną awarię za późno.

Epoka trzecia: test przed kodem

Dziś testy powstają u mnie przed kodem. Nie odkryłem tego sam – test-driven development ma na karku ćwierć wieku – ale długo uważałem, że to podejście dla purystów. Zmieniło się, gdy zrozumiałem, co ta kolejność robi z psychologią, o której pisałem wyżej.

Test pisany przed kodem przestaje być podatkiem doliczanym po robocie, a staje się definicją roboty. Najpierw zapisuję, co ma się stać – wliczając przypadki brzegowe, o których myślę, póki jeszcze myślę o wymaganiach, a nie o implementacji. Test jest czerwony, bo kodu nie ma. Potem piszę kod, aż będzie zielony. Nie ma momentu „dopisz test do gotowego” – tego momentu, w którym przez lata rodziły się moje luki.

Uczciwie: to nie jest pancerz. Test-first nie testuje wyobraźni. Jeśli o jakiejś ścieżce nie pomyślę, to nie powstanie ani jej obsługa, ani jej test – i pokrycie będzie wyglądać znakomicie, a dziura będzie czekać. Różnica polega na czym innym: luki przestały powstawać tam, gdzie mi się nie chciało, a zostały tylko tam, gdzie nie starczyło mi głowy. To wciąż luki – ale jest ich mniej i mają lepszą wymówkę.

Sekcja uczciwości: kiedy ręczne testowanie wciąż ma sens

Po takim wywodzie łatwo wpaść w drugą skrajność, więc uczciwie – ręcznie testuję nadal i są miejsca, gdzie to właściwe narzędzie:

  • Eksploracja. Test automatyczny sprawdza to, co przewidziałeś. Świadome, złośliwe klikanie po aplikacji – „a co, jeśli cofnę w połowie? a jak wkleję emoji w pole kwoty?” – znajduje problemy, których nie przewidział nikt. Z takiej sesji wychodzą pomysły na nowe testy automatyczne.
  • Ocena, nie weryfikacja. Czy formularz jest wygodny, czy komunikat błędu jest zrozumiały, czy animacja nie irytuje – to są pytania do człowieka, nie do asercji.
  • Rzeczy jednorazowe. Skrypt migracji, który uruchomię raz i wyrzucę, dostaje ręczną kontrolę wyniku, nie suitę testów.
  • Ostatni rzut oka przed wdrożeniem. Zielone testy plus dwie minuty klikania po świeżej wersji to nie nieufność wobec testów – to tania polisa na kategorię „wszystko działa, tylko wygląda źle”.

Wspólny mianownik: ręczny test jest dobry tam, gdzie potrzebna jest głowa, a zły tam, gdzie potrzebna jest powtarzalność. Ja przez ćwierć wieku używałem go do powtarzalności – i za to płaciłem.

Puenta

Trzy epoki, trzy tryby dowiadywania się o błędach. Ręcznie: mówiła mi produkcja – logami, użytkownikami albo ciszą. Testy po kodzie: mówiły mi testy, ale tylko tam, gdzie chciało mi się je napisać. Testy przed kodem: mówią mi testy, zanim kod w ogóle powstanie – a produkcji zostawiam już tylko to, czego nie umiałem przewidzieć.

W następnej części przechodzę od historii do warsztatu: co testuję jednostkowo, co integracyjnie, skąd naprawdę biorą się luki w pokryciu i dlaczego sto procent pokrycia nie jest moim celem. A ile tych testów jest w moim produkcyjnym projekcie – co do sztuki, z liczbami zamiast deklaracji – pokażę w osobnym wpisie z liczbami.