Matthew Carlson
Matthew CarlsonSr Consultant @ Industrial Logic, Inc.

Odblokowanie pełnej mocy testów

Sprawdź, kiedy testowanie oprogramowania naprawdę ma sens.
17.09.20214 min
Odblokowanie pełnej mocy testów

Istnieją cztery główne powody testowania oprogramowania, które moim zdaniem mają sens:

  • Sprawdzanie regresyjne (czy coś zepsułem)
  • Sprawdzanie logiczne (czy rzeczywiście kod robi to, co mi się wydaje)
  • Dokumentacja dynamiczna (testy pozwalają pokazać intencję wykonania kodu)
  • Informacja zwrotna na temat designu (trudność testowania, nieprzewidziane skutki uboczne, itp.)


Jednak wiele zespołów otrzymuje o wiele mniej pożytku z testowania, zatrzymując się już na sprawdzaniu regresji.

Moc testów regresyjnych

Nie ma wątpliwości, że sprawdzanie regresyjne jest ważne, aby upewnić się, że obecnie działające zachowanie będzie utrzymane. Zestaw testów regresyjnych to taka siatka zabezpieczająca, która istnieje w celu umożliwienia przyszłych zmian w kodzie. Bez nich musimy mieć cały system w głowie i zastanawiać się nad skutkami zmiany kodu. Jest to trudne nawet w przypadku małego programiku do zabawy i prawie niemożliwe w przypadku złożonego systemu. Gdyby tak nie było to nigdy nie pisalibyśmy bugów. Aby bezpiecznie utrzymywać, modyfikować i rozszerzać bazę kodu, musimy albo utrzymać ją w niezwykle prostej formie, albo przeprowadzić testy regresyjne.

Siła testów regresyjnych jest najbardziej widoczna w pewności, z jaką programiści wprowadzają zmiany. Pokazałem ludziom skomplikowany kod (Gilded Rose Kata) i zapytałem, jak długo potrwa modyfikacja kodu w celu dodania nowego typu elementu, bez wprowadzania błędu. Nawet jeśli odpowiednia część kodu składa się tylko z 50 wierszy, wspólne odpowiedzi wahają się od dnia do tygodnia.

Dodając kompleksowy zestaw testów charakteryzacyjnych (inaczej testy pinningowe) w celu określenia aktualnego zachowania przed dokonaniem zmian (łatwe do wykonania za pomocą ApprovalTest), liczba ta spada zazwyczaj do mniej niż jednej godziny. Dalsze ulepszenie designu przez użycie wzorca stanu, wspierane i zabezpieczone przez testy, zmniejsza szacunki do kilku minut. Pokazuje nam to, że ryzyko wprowadzenia zmian znacznie przewyższa wysiłek włożony w dokonanie zmiany. Posiadanie ochrony, jaką są testy, pozwala nam poruszać się znacznie szybciej.

Jednak większość zespołów (jeśli w ogóle piszą testy) zatrzymuje się tutaj. Dzieje się tak zazwyczaj dlatego, że piszą testy po fakcie. Czasami długo po fakcie. Aby uzyskać inne korzyści z testów, warto napisać je na początku. Jeśli ktoś napisze małą ilość kodu, następnie napisze test na ten kod w niezwykle zdyscyplinowanej, ciasnej pętli - nie więcej niż kilka minut - to może mieć to te same zalety. Nawyk testowania nam to ułatwia.

Sprawdzanie logiczne

Kolejnym celem testów jest logiczne sprawdzanie w szybkich interakcjach. Każdy, kto pisze kod od dłuższego czasu, chociaż raz zapewne zrobił błąd logiczny. Czy to proste użycie większe niż zamiast mniejsze niż, czy też błąd w bardziej złożonej logice rozgałęziania, wszyscy mamy jakiś na sumieniu. Jak i również późniejsze siedzenie i debugowanie, zastanawianie się, co się dzieje, bo "przecież to powinno po prostu zadziałać".

Rozpoczęcie od napisania testu, następnie oglądanie jak się nie udaje, a następnie napisanie kodu wystarczającego do zaliczenia tego testu, daje nam szybką informację zwrotną w takich sytuacjach. Widząc niepowodzenie testu zanim przejdzie, możemy mieć pewność, że nie mamy fałszywie pozytywnego wyniku i że to nasza logika sprawiła, że test przeszedł pomyślnie. Najpierw trzeba trochę czasu na napisanie testów. 

Jednak na dłuższą metę, jest to inwestycją, której warto się podjąć, żeby zaoszczędzić czasu i frustracji.

Komunikowanie dynamicznej intencji

Testy mają dodatkową zdolność do komunikowania naszych intencji. Jest to trzeci cel - dokumentacja dynamiczna. Podczas gdy statyczna intencja kodu może być udokumentowana w nazwach i komentarzach, intencja zachowania w trybie runtime jest łatwiejsza do wyrażenia poprzez testy. Każdy test może wyrazić zamierzone dynamiczne zachowanie oczekiwane dla danego scenariusza. Jest to przydatne nie tylko dla tych, którzy muszą zmienić kod w przyszłości lub zintegrować swój kod z naszym, ale także dla tych, którzy muszą udokumentować zachowanie w trybie runtime, API, itp. dla użytkowników. Testy stają się wyrazistymi przykładami.

Intencja jest często tracona przez czas, kiedy przechodzimy do pisania testów po fakcie. Jest to w 100% prawdziwe, jeśli ktoś inny napisze testy później. Mogą przekazać, co kod robi, ale nie jaki był jego cel. Testy napisane po utracie intencji są ściśle powiązane z bieżącą implementacją i koncentrują się raczej na pokryciu ścieżek kodu niż na zamierzonym zachowaniu. Z czasem może to prowadzić do trudnych do utrzymania testów. Każdy, kto spędził czas pisząc testy wokół legacy code, odczuł to.

Ulepszenie projektu

Ostatnim celem testów jest informacja zwrotna na temat designu. Gdy coś jest ciężkie do testowania to często oznacza, że ma ściśle powiązanych "niewygnodnych kolaboratorów". Zachowanie trudne do wyizolowania może wskazywać na niską spójność oraz kod oczekujący określonych efektów ubocznych. To nie są problemy testów - to problem ze złym projektem. Słuchanie tych informacji zwrotnych pozwala nam wprowadzać małe zmiany do projektu, które prowadzą do bardziej rozbudowanego, łatwiejszego w utrzymaniu i pozbawionego błędów kodu.

Wniosek

Odkryłem, że kiedy zespoły korzystają ze wszystkiego, co testy mają do zaoferowania, tworzą kod, który jest łatwiejszy w utrzymaniu, łatwiejszy do zrozumienia i łatwiejszy do rozszerzenia. Mniej czasu spędzają na bolesnym debugowaniu. Produkują oprogramowanie o wyższej jakości. Łatwiej jest im współpracować z osobami poza zespołem (np. zespoły odpowiadające za dokumentację, zespoły QA, InfoSec itp.). Są w stanie refaktoryzować w celu ulepszenia designu. Okazuje się również, że osiągnięcie dobrych wyników wymaga mniej czasu.

Dziękuję Billowi Wake'owi za informację zwrotną na temat tego postu.


Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>