Moich problemów z testami ciąg dalszy. (Nienawidzę ich...)
Spotkałem się z co najmniej dwoma podejściami:
-
Testy muszą być w opór dokładne, mają sprawdzać każdy możliwy edge case, mają ogarniać całą funkcjonalność aplikacji. Albowiem: (a) testy same w sobie stanowią specyfikację aplikacji; (b) to testy informują o tym, czy aplikacja jest poprawna, zatem każdy nieprzetestowany obszar czy edge case z definicji nie działa. (Z tego wynika także, że unit testy nie wystarczają: potrzebne są tylko / a może wręcz jedynie / testy na poziomie modułu i testy end to end; co oznacza, że aplikacja GUI nie działa, dopóki nie postawimy bota klikającego w nią)
-
Testy pisane według punktu 1. wymagają gigantycznych ilości czasu i wysiłku. Zazwyczaj nie jest to opłacalne. W większości wypadków straty przedsiębiorstwa nie będą dotkliwe, jeśli poprawność zejdzie z 99.9% na 99%, natomiast poświęcenie 2x (4x, 10x) więcej roboczogodzin na pisanie testów już będzie dotkliwą stratą. Dlatego testy powinny sprawdzać tylko "big picture", czy aplikacja w ogólności działa, bez wchodzenia w multum szczegółów.
Podejścia drugiego nie rozumiem: wydaje mi się, że zamiast dwóch pieczeni (gwarancja poprawności i rozsądne ilości czasu i pracy) osiąga dwa trupy. To, czy aplikacja w ogólności działa, to po prostu widać. Nie trzeba pisac testów, by to sprawdzic. (Niezależnie od tego, czy piszemy testy czy nie, i tak nie unikniemy ręcznego uruchomienia naszej apki prędzej czy później!) To właśnie rozmaite edge case'y to jest to, co jest trudne do ręcznego sprawdzania i z tego powodu wartość dodaną zdaje się wnosić właśnie sprawdzanie edge case'ów. Sensem i celem pisania testów automatycznych jest właśnie gwarancja poprawności: chcemy wiedzieć, że jeśli testy przechodzą, to apka działa, i nie trzeba więcej nic sprawdzać, można puszczać na produkcję. Ale testy pisane według punktu 2. nie mogą dać takiej gwarancji. Zatem takie testy nic nie wnoszą ale nadal wymagają poświęcenia czasu na ich pisanie i utrzymywanie.
Z drugiej strony pozostaje faktem, ze podejście pierwsze wymaga gigantycznych ilości roboczogodzin. Możliwych kombinacji, które mogą cos popsuć (w zależności od zastosowania) może być przecież wykładniczo wiele. Co więcej, trudno zoptymalizować takie testy (unit testy juz łatwiej się optymalizuje), a skoro z założenia w tym podejściu testów ma być wiele, to łatwo o problemy wydajnościowe. I chociaż w internecie wielu gorliwie optuje za testami według punktu 1., to jednak nie można zaprzeczyć, że dwukrotne wydłużenie czasu pisania aplikacji rzeczywiście jest dotkliwą stratą dla firmy i nie można także zaprzeczyć, że w wielu zastosowaniach jest kompletnie nieistotne, czy apka będzie się crashować raz na rok czy raz na miesiąc.
Oczywiście, testy według punktu 1. są niezbędne, jeśli jakikolwiek błąd w działaniu apki będzie miał dramatyczne konsekwencje. Ale nie każdy program to sterownik rozrusznika serca albo komputer pokładowy statku kosmicznego.
Z punktem 1. jest jeszcze jeden problem: z definicji edge case'y sa trudne do zauważenia, a jeśli nie widzimy, że jakiś problem może występować, to trudno pisać test gwarantujący, że ten problem nie wystąpi. Zatem nawet najbardziej szczegółowe testy nie gwarantują poprawności. Niektóre problemy mają niemiły zwyczaj objawiać się dopiero podczas używania aplikacji. (M.in. problemy związane ze współbieżnością, problemy występujące przy pisaniu programów próbujących synchronizować jakoś działanie niezależnych od siebie zewnętrznych serwisów / aplikacji, gdzie dziwactwa w działaniu tych aplikacji mogą stwarzac problemy, etc etc).
Ale jeśli punkt 1. odrzucamy jako nieopłacalny, a punkt 2. odrzucamy jako bezsensowny, to co nam pozostaje? Brak testów?
A może raczej pisać testy przeciwko tym edge case'om, od których się odbijemy już po napisaniu aplikacji? Jeśli jakiś problem został przeoczony podczas pisania aplikacji, to wtedy może jest sens zagwarantować, że nie zostanie on przeoczony nigdy więcej? Takie testy oczywiście nie będą mogły dać gwarancji, że program działana (bo z definicji testujemy tylko pojedyncze problemy, a nie całość oczekiwanego działania), ale w przeciwieństwie do testów według punktu 2. zdają się jednak wnosić jakąś wartość dodaną.
A może to ja się mylę? Są szefowie, którzy wymagają właśnie testów takich, jakie opisałem w punkcie 2. Byc może punkt 2 jednak nie jest aż tak bezsensowny, jak mi się zdawało: jeśli aplikacja ma liczne ficzery, to (choćby tylko powierzchowne) sprawdzenie ich wszystkich nadal jednak zwalnia nas z klikania. Tak, będą nieprzetestowane edge case'y, ale nie przed tym się bronimy, a tylko przed takimi ewentualnościami, że wprowadzenie jakieś pozornie niewinnej zmiany w jednym ficzerze całkowicie wali inny ficzer. Oczywiście problemy wykryte przez tak powierzchowne testy bardzo szybko objawiłyby się i bez tych testów (bo z definicji, jeśli takie testy nie przechodzą, to program w ogóle nie działa), ale o to właśnie chodzi, żeby przez przypadek nie zwalić całkowicie produkcji. Błędy, które będą wykryte przez takie testy, są bardzo groźne właśnie przez swą grubość. Wbrew temu, co pisałem wyżej, takie testy jednak wnoszą wartość dodaną: mamy gwarancję, że nie położymy wszystkiego bez konieczności przeklikiwania się przez całość aplikacji. (W zasadzie chyba jedyna sytuacja, przed którą takie testy by broniły, to taka sytuacja, że - jeśli np. piszemy Worda - zmiana w funkcji liczącej słowa powoduje wyjątki, gdy chcemy pogrubić tekst -- ale moze tego rodzaju sytuacje rzeczywiście się zdarzają, więc bronienie się przed nimi może i wnosi wartość dodaną?)
Jednak znowu: nawet wtedy unit testy wydają się być bezsensowne. Wartość dodaną wniesie tylko bot klikający w naszą apkę. Bo tylko to zagwarantuje to, co takie testy mają za zadanie zagwarantować - że w ogólności program działa i nie jest całkowicie zwalony.