Jak szczegółowe winny być testy?

1

Moich problemów z testami ciąg dalszy. (Nienawidzę ich...)

Spotkałem się z co najmniej dwoma podejściami:

  1. 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ą)

  2. 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.

4

Ja sugeruje zacząć od:

  • Testy akceptacyjne, czyli każdy use-case ma swój zestaw testów, tak że po puszczeniu ich wiesz ze dany ficzer działa, przynajmniej w podstawowej wersji
  • Testy regresji, czyli jak trafiliście na edge case to dopisujesz testy które go sprawdzają

Dalej to już według potrzeb - jak jest kawałek złożone logiki, to moze warto napisać do niego więcej testów.

Wartość dodaną wniesie tylko bot klikający w naszą apkę

A jak na poziomie testów wysyłasz takie same requesty do apki jak ten klikajacy bot? ;) Koszt dużo niższy, a pewność że działa nadal dość wysoka.

5

Tests (1).png
Na czerwono - Testujemy według pkt. 2 (pobieżnie)
Na niebiesko - Według pkt. 1 (rygorystycznie)

4
kmph napisał(a):

Ale jeśli punkt 1. odrzucamy jako nieopłacalny, a punkt 2. odrzucamy jako bezsensowny, to co nam pozostaje? Brak testów?

Raczej zdrowy rozsądek.
Podejście z punktu 1 zastosować do testów jednostkowych, w nich pokryć "każdy" przypadek brzegowy i typowe scenariusze użycia.
Podejście z punktu 2 zastosować do testów integracyjnych czy tam e2e, w nich sprawdzić, czy dany przypadek użycia w ogóle się uruchamia i działa dla optymistycznych scenariuszy.

Przykład dla 1, to np. kalkulator kwoty na fakturze - jednostka za to odpowiadająca musi sprawdzić, czy lista pozycji faktury nie jest pusta, czy wszędzie podano kwoty, i czy są nieujemne, czy stawki VAT istnieją, itd, i do tego wszystkiego da się napisać łatwo i szybko testy pokrywające wszystkie przypadki.
Przykład dla 2 to endpoint zwracający PDFa z fakturą. Tu wystarczy sprawdzić, czy:

  1. dla istniejącego ID faktury i zalogowanego użytkownika dostaniemy 200 i response zawierający PDF;
  2. dla istniejącego ID faktury i niezalogowanego użytkownika dostaniemy 401;
  3. dla nieistniejącego ID faktury i zalogowanego użytkownika dostaniemy 404.

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?

Oczywiście - pierwszym krokiem naprawy buga jest napisanie testu, który ten błąd pokazuje. Potem można naprawić właściwy kod, no i dopóki nie usuniemy testu, to nie ma szans, że dokładnie ten sam błąd się powtórzy.

3

Oprócz tego co zostało napisane, pomyślałbym o TDD.

Jasne, że jest kod super hiper ważny, np dotyczący transakcji, pieniędzy, kluczowych algorytmów, gdzie chcesz przetestować go na 130%, i kod który jak się coś popsuje nic się nie stanie (przycisk będzie zielony zamiast niebieskiego). Trzeba do tego podejść z rozsądkiem.

Jeżeli masz logikę do testowania, to zwykle lepiej i wydajniej jest ją testować jednostkowo (czy to testując klasę czy grupę klas - jednostkę kodu). W systemie, który ma dużo logiki klasyczna piramida testów sprawdza się znakomicie. Jak masz logikę aplikacji, mapery, przelotki to lepiej to testować End to end i/lub integracyjnie. W systemie readonly, albo bez logiki sprawdzi się raczej odwrócona piramida z mnóstwem testów e2e i małą liczbą jednostkowych.

Myślę, że największy problem to aplikowanie tego rozsądku i ludzie, którzy nauczyli się testować na jednym przypadku w danym kontekście i później mówią że albo testy jednostkowe są złe i nie potrzebne i tylko testy integracyjne dają im pewność ze system działa albo że testy integracyjne są złe, nie można im ufać i tylko jednostkowo warto testować. A tak naprawdę potrzebujemy wszystkich rodzajów testów, aplikowanych w każdym przypadku w innych proporcjach. Plus dziś zwłaszcza w świecie chmury i micro serwisów równie ważny jest monitoring i testy na produkcji ( które nie polegają na odpalaniu testów na produkcji i braku testów wcześniej, a raczej monitorowaniu jej i testach typu A/B testing, canary releases, feature toggles itp praktykach).

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0