
Krok 5 | Po co nam testy – 5 kroków do zagłady
W poprzedniej części zobaczyliśmy jak praca na warstwach sprawia, że kod jest naprawdę poważny i elastyczny. Jesteśmy gotowi na wszystko. Co prawda nie sprzyja to pisaniu testów, ale po co nam one?
Wpis jest częścią cyklu „5 kroków do zagłady”. W tym momencie dostępne wpisy to:
- Wstęp
- Krok 1 – długie metody
- Krok 2 – Komentarze po całości
- Krok 3 – Wielopokoleniowy kod
- Krok 4 – Kult warstw
- Krok 5 – Po co nam testy
Seria dostępna jest też w formie wideo na moim kanale w serwisie Youtube.
Jesteśmy poważnymi programistami, co nie? Zatrudniają nas po to żebyśmy robili to czego się wymaga? Znamy się na swoim fachu? To czemu mielibyśmy marnować czas na pisanie kodu, który ani nie pójdzie na produkcję, ani nie doda nowej funkcjonalności. Do tego wymaga masę czasu, a tylko potwierdza to co bez tego wiedzieliśmy, czyli fakt, że nasz kod oczywiście działa.
To co robimy mamy opisane w komentarzach. Funkcjonalności ładnie opakowaliśmy w konkretne funkcje. Dzięki dziedziczeniu nie mamy duplikacji kodu więc wszystko jest zminimalizowane. No i oczywiście mamy warstwy więc znowu wszystko jest uporządkowane, wiadomo co z czym działa, wystarczy to w komentarzach opisać i wszyscy sprawnie mogą z tym pracować. Robiąc zmiany, skoro już jest taka potrzeba robi się na małych klasach, dobrze udokumentowanych komentarzami. Nie ma możliwości popełnić błędu. A skoro nam tyle płacą to przecież ośmieszylibyśmy się robiąc błąd w tak prostym kodzie!
Z resztą nie mamy czasu na takie głupoty. Więcej nowych funkcjonalności to większy zarobek firmy, więc możemy iść po podwyżkę co kilka miesięcy. A jak niby by to miało wyglądać kiedy połowę czasu będziemy marnować na pisanie kodu, który nikomu się nie przyda? Prędzej by nas zwolnili niż dali podwyżkę.
Problem
W ostatniej części tego cyklu nie ma przykładowego kodu bo skoro nie piszemy testów to nie mamy kodu tych testów. A myślenie zaprezentowane powyżej jest niepokojąco częste. I to nie tylko jest związane z narracją prowadzoną przez pracodawców bo to się zdarza jeżeli ktoś zatrudnia programistów, a nie wie jak się tworzy oprogramowanie. Najgorsze jest to, że taka narracja jest też prowadzona świadomie przez programistów! A co z takiego podejścia wynika?
Degradacja jakości
Nie pisząc testów bardzo łatwo jest napisać kod, który w przyszłości będzie trudny w utrzymaniu i rozbudowie. Bo to właśnie testy są tym pierwszym sprawdzianem czy to co piszemy jest elastyczne. W testach mamy często potrzebę coś podmienić, czegoś użyć niezależnie. Jeżeli nasz kod nie jest gotowy na takie sytuacje to nie damy rady napisać testów do niego.
Z mojego doświadczenia mogę powiedzieć, że wystarczy spojrzeć czy w projekcie jest jakiś folder z testami żeby mieć obraz tego jak łatwo będzie wprowadzać zmiany w tym projekcie. Bo za każdym razem kiedy tych testów nie było to potem kod okazywał się mocnym spagetti, do którego dodanie czegokolwiek wymagało masy kombinowania i szukania obejść problemów.
Brak „żywej” dokumentacji
Dokumentacja w postaci komentarzy lub plików tekstowych to jedno. Ale jak już mówiliśmy w części o komentarzach jednym z ich problemów jest konieczność pamiętania o aktualizacji. Z testami jest inaczej. Bo pokazują one jak faktycznie działa system w danym momencie. Kiedy otworzymy sobie pliki testów to mamy jasno pokazane co się stanie w sytuacji kiedy jakieś dane przekażemy. Albo jakie w ogóle zadania są realizowane w systemie. I jeżeli tylko ktoś nie postanowi tych testów wyłączyć to nie ma możliwości, żeby wprowadzić jakąś zmianę i nie uwzględnić jej w tych własnie testach. W końcu bez tego build automatyczny nie przejdzie albo nasz pull request nie przejdzie code review.
Przewaga testów w porównaniu do standardowej dokumentacji jest taka, że wprowadzanie w nich zmian nie wymaga przeredagowania całości tylko dodanie lub zmienienie bardzo małych fragmentów. Mając dokumentację „papierową” jednak trzeba poświęcić więcej czasu na napisanie chociażby kilku zdań od nowa. W testach operujemy językiem, który znamy najlepiej czyli językiem programowania.
Rezygnując z testów rezygnujemy więc z takiej żywej, łatwiejszej w utrzymaniu dokumentacji.
Niepewność zmian
Największy, najpopularniejszy i najpoważniejszy problem. Systemy stają się coraz większe. Coraz bardziej złożone. Pracujemy nad nimi w zespołach. Do tego niejednokrotnie składamy kod w całość za pomocą wstrzykiwania zależności albo pracujemy nad pojedynczymi klasami. Więc nawet jak mamy pewność, że to co napisaliśmy w danym momencie działa to jaką mamy pewność, że naszej klasy ktoś nie użył wcześniej w innym miejscu i nie opierał się o jej pierwotną wersję zachowania? Sprawdzanie tego za każdym razem to masa straconego czasu. Do tego nie zawsze taka operacja jest możliwa. Bo co jeśli podzieliliśmy projekt na biblioteki, którymi zarządzamy przez np. repozytorium NuGetowe więc w innej części systemu są one zewnętrzną zależnością? Albo w ogóle co w sytuacji kiedy zaktualizowaliśmy jakąś zewnętrzną bibliotekę i zaczęła ona trochę inaczej działać? Chcielibyśmy się o tym przekonać dopiero na produkcji albo po kilku tygodniach od wprowadzenia zmiany, w trakcie testów manualnych?
Testy dają nam w kilka sekund odpowiedź czy wprowadzona zmiana nie wpłynęła negatywnie na działanie tego czego nie ruszaliśmy. Im większa złożoność aplikacji tym lepiej tą zaletę widać. I tym lepiej też widać problem kiedy tych testów nie ma. Bo w takim przypadku błędy i czas poświęcony na szukanie przyczyny potrafią być naprawdę spektakularne.
Brak testów to także brak możliwości przeprowadzania bezpiecznego refaktoringu. Bo w jaki sposób będziesz miał pewność, że po przeniesieniu albo zmodyfikowaniu danego fragmentu działa on tak samo jak przez zmianami? Będziesz za każdym razem uruchamiał aplikację z różnymi parametrami, zapisywał wyniki i potem robił to samo po zmianach? To właśnie robisz to samo co testy tylko w najgorszy możliwy sposób. Mając testy mógłbyś na bieżąco potwierdzać czy zmiany nie wpłynęły negatywnie na jakiekolwiek wyniki.
Przyczyna
Jedną z przyczyn już podałem opisując problem. Jest to brak wiedzy na temat wytwarzania oprogramowania. Myślenie o kodzie w kategoriach „dodaje funkcjonalność albo nie dodaje”. Bez myślenia o tym czy dodatkowy kod nie uchroni nas przypadkiem przed poważniejszą tragedią. Brak tego myślenia w dalszej perspektywie widać też kiedy ktoś mówi, że testowanie wymaga więcej czasu na zadanie bo trzeba napisać więcej kodu i w dodatku poświęcić dodatkowy czas na zastanowienie się jak coś przetestować.
Ale jak już wspomniałem brak testów to droga do degradacji jakości. A jakość kodu bezpośrednio wpływa na tempo pracy. Bo rozplątywanie zawiłego kodu potrafi pochłonąć masę czasu. Druga sprawa to taka, że brak testów to duże ryzyko powstania błędów. A naprawianie błędów to kolejna rzecz, która potrafi pochłonąć ogromną ilość czasu, zwłaszcza w połączeniu z zawiłym kodem. I wykrycie czegoś na produkcji sprawia, że nagle trzeba jakiegoś programistę odciągnąć nawet na kilka dni od dostarczania nowych funkcjonalności po to żeby zanurzył się w kodzie i spróbował znaleźć przyczynę problemu. A że będzie to robił po dłuższym czasie od wprowadzenia zmiany to może się okazać, że teraz to naprawa nie jest już taka prosta bo źródło błędu obrosło masą dodatkowego kodu, który w międzyczasie powstał.
Rozwiązanie
Przypadek testów jest ciężki. Bo jeżeli trafimy na kiepskie przykłady jak testować to szybko się zniechęcimy.
To co na pewno polecam to nie robienie czegoś takiego, że najpierw piszesz kod, a potem wszystkie testy. Bo znam to podejście, sam je widzę na co dzień i to bardzo demotywuje. Nie rób tak. Jeżeli masz dużo chęci i naprawdę zależy Ci na jakości to zapoznaj się z techniką TDD czyli Test Driven Design. I przynajmniej przez jakiś czas się do niej stosuj. Kiedy pisanie testów będzie dla Ciebie naturalne, tzn. będziesz potrafił w locie wymyślić przypadki testowe i ich strukturę to możesz od tego odejść i pisać testy po napisaniu kodu. Ale nadal staraj się wtedy pisać testy po każdej napisanej funkcji czy klasie.
To co jest też w tym temacie rekomendowane, nawet jeżeli wcześniej nie mieliście testów to dopisywanie testu do tego co się zepsuło. Przykładowo macie błąd w liczeniu VATu na fakturach. Jest to jakaś funkcja. To naprawiając ten błąd napisz testy tylko do tej funkcji. Tak żeby pokrywały przypadek, który pierwotnie powodował błąd. W ten sposób stopniowo zwiększasz pokrycie kodu testami i jednocześnie zabezpieczasz kolejne fragmenty przed powtórzeniem się już raz odkrytego problemu.
I pamiętaj – nie chodzi o to żeby mieć 100% pokrycia kodu testami ale o to żeby te testy faktycznie dawały wartość. Dlatego najważniejsze to edukacja, patrzenie jak to robią inni, sprawdzanie na własnych przykładach i jeszcze raz edukacja.
Zakończenie
I to tyle w serii 5 kroków do zagłady kodu. Przeszliśmy w niej przez 5 problemów, które na początku nie wydają się tak groźne ale po czasie albo kiedy występują razem zbierają się w ogromną przeszkodę przy próbach utrzymania lub rozbudowywania aplikacji. Często o nich nie myślimy na co dzień albo mamy na ich temat błędne przekonania. Niejednokrotnie wystąpienie tych kroków wynika z faktu, że nie widzieliśmy wcześniej tragedii jaką spowodowały albo, że nie musieliśmy z nimi walczyć wraz z rozrastaniem się naszego kodu. Najczęściej te kroki pojawiają się kiedy aplikacja jest mała więc nie zwracamy na nie uwagi. Albo uważamy, że nie są groźne. A one wracają po miesiącach czy latach rozwoju programu i potrafią nawet położyć nasz biznes.
Tak więc moja zbiorcza rada:
Eliminuj te błędy małymi krokami, bez rewolucji, która szybko gasi zapał. Postaw na edukację całego zespołu pokazując wszystkim dlaczego drobne zmiany są tak ważne. I ucz się na własnych przykładach.
Bardzo fajny artykuł, znam wiele osób które powinny go przeczytać :) Niestety, nie każdy przykłada taką wagę to testowania swojego kodu