F.I.R.S.T. - jak pisać unit testy lepiej.
F.I.R.S.T. to pewien standard, jaki unit testy powinny spełniać. To zbiór założeń, które wyznaczają kierunek, jaki powinniśmy obierać, pisząc testy.
F jak Fast
Czas oczekiwania na wynik naszych unit testów powinien być jak najkrótszy.
Znani i cenieni specjaliści, jak Robert C. Martin czy Kent Beck, w swoich książkach przytaczają związek między czasem wykonywania testów, a ich regularnym uruchamianiem. Jeśli testy “kręcą się” kilka lub kilkanaście minut, często zniechęca to programistów do regularnego ich uruchamiania. Dodatkowo, gdy weźmiemy pod uwagę Test Driven Development, to praktycznie paraliżuje to rozwój kodu i rodzi sporo frustracji.
Kiedy testy “kręcą się” za długo? Tutaj sprawa już nie jest taka prosta. Czy 5 s jest OK? Myślę, że tak. 30 s – jeszcze akceptowalne. 1–2 min? Tutaj już może pojawić się myśl: “Czy zdążę zrobić sobie kawę/herbatę?” Gdy zaczynamy myśleć o zrobieniu czegoś innego, oczekując na wyniki unit testów, to już jest znak, że trwa to za długo. Gdy pracujemy w TDD, to zmiany często są minimalne, trwające kilka sekund. Nie możemy pozwolić, by ich weryfikacja trwała kilkukrotnie dłużej, bo wybije nas to z rytmu.
Przyczyn długiego oczekiwania na zakończenie unit testów może być kilka:
1) Sleepy w testach. Jeśli w naszych unit testach korzystamy z czasowych opóźnień, to często jest to związane z timerami użytymi w logice naszego kodu.
2) Dostęp do plików. Pojedynczy przypadek raczej nie wpłynie znacząco na czas wykonywania unit testów. Gdy takich odczytów jest więcej, zaczynają one mieć znaczenie.
3) Zewnętrzny framework do przesyłania message’y/eventów. Jeśli nasz kod produkcyjny korzysta z takich rozwiązań, może to w testach doprowadzić do opóźnień. Na przykład, gdy message nie przyjdzie na czas z powodu obciążenia sprzętu, na którym uruchamiamy testy (współdzielony serwer).
Z pewnością każdy z Was może znaleźć też inne przyczyny opóźnień. Najczęstszym rozwiązaniem jest wprowadzenie warstwy pośredniej, rodzaj interfejsu, aby móc zastąpić implementację problematycznych zależności mockami.
Warto pamiętać i dążyć do tego, aby czas oczekiwania na wyniki unit testów był jak najkrótszy. Podnosi to nie tylko jakość kodu, ale też satysfakcję z samej pracy z nim.
I jak Independent
Unit testy powinny być niezależne od siebie nawzajem, tak aby można było uruchomić je w dowolnej kolejności.
Sytuacja, w której jeden test nie przechodzi tylko dlatego, że inny również nie przeszedł, nie należy do zbyt komfortowych. Tracimy wtedy wiarę w wiarygodność testów. Dodatkowo zmiana w jednym teście wymusza zmianę również w innym. Framework testowy Google Test domyślnie uruchamia testy w sposób losowy, dzięki czemu złamanie tej reguły powinno wyjść bardzo szybko.
Częstym powodem zależności między testami są zmienne globalne. Istnieją techniki odcinania zależności od zmiennych globalnych czy wolnych funkcji (niezwiązanych z żadnym obiektem). W mojej ocenie jedną z najlepszych jest opakowanie użycia zmiennej globalnej (czy też funkcji) w metodę klasy w sekcji protected. Tak, aby można było przysłonić jej zachowanie w testach, tworząc klasę Testable.
Technik radzenia sobie ze zmiennymi globalnymi jest więcej i można je znaleźć w tak świetnych książkach jak “Praca z zastanym kodem” czy “Refaktoryzacja. Ulepszanie struktury istniejącego kodu”.
R jak Repeatable
Testy powinny być powtarzalne, niezależnie od środowiska, w którym je uruchomimy. Czy to będzie mój laptop, czy serwer firmowy – wyniki testów powinny być takie same. Esencją braku tej zasady jest znane przez chyba wszystkich programistów zdanie: “U mnie działa.” Świetnie, ale testy powinny działać wszędzie tam, gdzie się je uruchomi, i zwracać to samo. Gdy unit test zwraca jeden wynik w środowisku A, a inny wynik w środowisku B – to znak, że ma on jakąś zależność, która powinna zostać odcięta.
Tutaj znów przyczyną może być dostęp do systemu plików. Gdy w jednym środowisku pliki istnieją, a w innym nie – testy zachowują się inaczej.
Inną przyczyną może być korzystanie ze zmiennych środowiskowych. Tak czy inaczej – takie praktyki przeczą idei unit testów, jaką jest izolacja: odcięcie zewnętrznych zależności i testowanie małego fragmentu w przygotowanym do tego środowisku i scenariuszu.
W mojej ocenie korzystanie z zewnętrznych zależności w testach to droga na skróty, która w dłuższej perspektywie rodzi więcej problemów, niż daje korzyści.
S jak Self-Validating
Unit test powinien zwracać jednoznaczny wynik – test się powiódł lub nie. Testy jednostkowe to jedno z wielu zautomatyzowanych narzędzi wspierających naszą pracę z kodem i podnoszących jego jakość.
Gdy musimy ręcznie weryfikować wyniki testów, marnujemy sporo czasu lub co gorasze, możemy błędnie odczytać ich wynik – dostarczając wadliwy kod lub niepotrzebnie debuggując go, gdy jednak jest poprawny, szukając błędu, który nie istnieje. Jeśli test wymaga ręcznego sprawdzenia logów, by potwierdzić, czy testowany kod działa, to znaczy, że coś jest nie tak.
Czy możemy wtedy mówić o zautomatyzowanym teście? Zdecydowanie nie. Informacja zwrotna powinna być jasna – Twój kod działa/nie działa. Myślę, że ten punkt jest jasny i bez niego nie możemy mówić o cyklu TDD: Red → Green → Refactor. Bez jasnego sygnału, jak zakończyły się testy, nie możemy płynnie pracować w tym rytmie.
T jak Timely
Unit testy powinny być uruchamiane w odpowiednim czasie. W tym punkcie nie mówię o czasie wykonywania, ale o momencie, w którym uruchamiamy testy.
W Test Driven Development (TDD) mamy cykl Red → Green → Refactor, o którym już wspomniałem. Każdy cykl rozpoczyna się od napisania testu i od razu próby jego uruchomienia. Najczęściej kończy się on błędem kompilacji, gdyż nowa metoda, którą chcemy przetestować, jeszcze nie powstała. To też jest wynik “Red”.
Testy uruchamiamy tak często, jak to możliwe, i wprowadzamy zmiany iteracyjnie, małymi krokami. Dzięki takim krótkim cyklom szybko możemy wykryć regresję, a zakres zmian jest minimalny i łatwo możemy dojść do tego, gdzie popełniliśmy błąd. Timely nie odnosi się już do tego, jak pisać unit testy, tylko jak ich używać - często :)
Podsumowanie
W tym wpisie starałem się przybliżyć pięć cech dobrych unit testów. Są to drogowskazy pomagające nam nie tylko pisać testy lepiej, ale przede wszystkim pracować z nimi na co dzień. Trzymając się tych reguł, z pewnością odczujemy różnicę w codziennej pracy – zyskując kontrolę nad zmianami, większe zaufanie do kodu i pewność, że nie wprowadzimy regresji.
Autor: Tadeusz Biela
Programista C++ | Entuzjasta TDD | Fan unit testów
Zostaw komentarz