Professional Documents
Culture Documents
Frontendowych
Design Testów
Frontendowych
Po co nam testy? To oczywiste! Testy piszemy po to, aby chroniły nas przed regresją
funkcjonalności, aby refactory kodu były stabilne i - może przede wszystkim - aby
długoterminowe utrzymywanie aplikacji było łatwiejsze.
😉
Hmm, łatwo powiedzieć (lub napisać) ... ale kiedy zejdziemy do poziomu szczegółów,
😉
to sprawy przestają być takie oczywiste. Z frazesami jest zawsze łatwo . Dodatkowo
- czynniki takie jak np. „łatwość utrzymania” dla różnych developerów - a także w zależ-
ności od specyfiki projektu - może oznaczać co innego. I w efekcie pisanie „wartościo-
wych testów” będzie wyglądało inaczej. Co jest „dobre”, a co „złe” - jest kontekstowe.
Klasyczna piramida testów sugeruje, aby wśród testów najliczniejsze były testy
jednostkowe, w drugiej kolejności - integracyjnych, a najmniej - E2E. Wynika to przede
wszystkim z założenia, że im większy obszar systemu testujemy, tym dłużej test będzie
trwał - oraz że tym bardziej taki test będzie kruchy. Bo skoro przechodzi przez więcej
elementów, to ma więcej powodów, aby się wywalić, jeśli którykolwiek element się
zmieni. Logiczne.
Ale - wróćmy do meritum - żeby przetestować, czy CAŁOŚĆ działa, to i tak trzeba
dodać jeszcze testy integracyjne. Bo nasze kliknięcie np. przechodzi w wywołanie
callbacka, callback dispatchuje thunka/sagę/observabla/..., ten zmienia się w akcję do
reduxa - i gdzieś hen na końcu jest dopiero strzał HTTP. Tak jak poniżej:
I pytanie - skoro i tak musimy przetestować całą ścieżkę z góry na dół, aby mieć
pewność, że całość bangla: to jak wiele daje nam dodatkowe pokrywanie każdego unita
jego osobnymi testami?
Owszem, test jednostkowy w przypadku regresji powie mi „o tu, tutaj jest błąd, źle
wywołałeś callbacka typie! Patrz, o tu, widzisz?”. Ale po pierwsze: nie powie mi,
która konkretnie funkcjonalność na tej regresji ucierpi. Po drugie: nie powie mi, czy
💸
ktokolwiek w ogóle ucierpi. I po trzecie: każdy test ma swój koszt obsługi . Nie tylko
jego uruchamianie trwa (czas długości trwania testów), ale także - jego utrzymywanie
kosztuje (nasz czas pracy). W efekcie, podejście typu „im więcej testów tym lepiej” to
🤭
- cytując klasyka: ani świento prawda, ani tys prawda - tylko g. prawda . Nie należy
danej funkcjonalności pokrywać testami takimi, śmakimi i owakimi, w nadzieji że będą
„lepiej” przetestowane - bo to zwiększa koszty, ale regresje, które wychwytujemy są
potencjalnie nadal takie same.
To jest w ogóle uniwersalne pytanie odnoszące się do architektury: CZY WARTO. Może
- jak ktoś jest wzrokowcem, to bardziej się wryje w pamięć - powtórzmy nasze jedno,
„zajebiście ważne pytanie”:
Czy warto?
I w kontekście testowania UI - czy oprócz testów integracyjnych zawsze pisać także
jednostkowe? Zawsze? Z reguły?
Coraz więcej osób, m.in. twórca React Testing Library, Kent Dodds, dochodzi do wniosku,
że niekoniecznie. Kent w ogóle kwestionuje kształt klasycznej piramidy testów -
i w zamian proponuje testing trophy, które na pierwszym miejscu stawia właśnie testy
integracyjne.
Dlaczego? Bo:
y testy integracyjne nie obchodzi „jak działa każdy element po drodze”, tylko czy
wszystkie składowe się poprawnie ze sobą integrują. A testy jednostkowe mają
to do siebie, że jeśli zmienia się implementacja unita, to często sypią się jego testy.
W konsekwencji:
y czy należy całkowicie olać testy jednostkowe? A skąd! Absolutnie nie! Chodzi o to,
aby unitów nie stawiać ich na pierwszym miejscu.
TL;DR; testy jednostkowe nie są złe. Czasem (np. snapshoty reducerów) są super. Sztuka
w dobraniu odpowiednich proporcji.
Czy obalam? Raczej nie. Bardziej kwestionuję. Właśnie o to chodzi - nie daję gotowych
odpowiedzi - tylko stawiam pytania. Skonfrontuj pytania, które stawiamy w tym
tekście, a także praktyki - z własnym projektem! I sam(a) zdecyduj, co będzie miało
najwięcej sensu, bo to Ty znasz swój projekt najlepiej. Staram się kierować - tu znowu
cytat z klasyka:
i każdemu to polecam 😉.
DESIGN-FIRST.
Z czego to wynika? Możemy się pomylić np. dobierając odpowiednia asercję związaną
z DOMem albo ze sprawdzaniem mockowych funkcji (tj. jest.spy). Nie z głupoty,
i nie z ignorancji - możemy pomylić się tak po prostu... bo każdy się może pomylić - i ja,
i Ty. Możemy nieopatrznie wprowadzić do naszego kodu tzw. FALSE NEGATIVE (lub,
rekomendowałbym inne określenie: FALSE PASS). Czyli test, który nie wybucha,
a powinien. O tym wszystkim będziemy obszernie mówili w Architekturze na Froncie.
Jak się przed tym ochronić? Jak już zmóżdżysz, jaka powinna być struktura testu, jakie
zachowanie powinno być badane itp - to np. tymczasowo odwróć sytuację (przekaż
innego propsa, inną flagę, etc.) - i upewnij się, że Twój test na pewno failuje. Jeśli failuje
y z nazw testów nic nie wynika, bo np. ktoś wpisał ComponentX should work
🤬
correctly... Jakby k wa ktokolwiek oczekiwał, że bedzie inaczej...
y mockowane jest tak dużo, że nie wiadomo, co faktycznie bierze udział w teście.
Czyżby testowane były mocki? 😯
y testy są skomplikowane. Trzeba najpierw się zdoktoryzować z setupu testów (np.
bazujemy na rxjs marble tests), aby w ogóle zrozumieć, co się w nich dzieje.
y testy sprawdzają wiele rzeczy na raz. Masz takich kilka testów w projekcie, które
co sprint się kładą i trzeba w nich - co rusz - dłubać i aktualizować? Przyczyn
może być wiele - ale skoro test często failuje, to być może ma wiele powodów do
failowania? Innymi słowy - testowanych jest jednocześnie wiele funkcjonalności -
i zmiana którejkolwiek z nich skuktuje wywaleniem się - na okrągło - tego samego
testu?
Jeśli chcesz aby testowanie kojarzyło Ci się z czymś przyjemnym i dającym satysfakcję,
to każdy z ww. punktów zaneguj. A jeśli testy chcesz znienawidzić - Ty albo Twoje
😈
ziomeczki - to zostaw jak jest .
y zmiana implementacji nie wpływa na funkcjonalność - ale test failuje? Test jest
😓
zależny od implementacji (i jest kruchy ).
y potrzebujesz zmodyfikować fake data na potrzeby jednego testu, ale tych samych
danych używają inne testy i teraz musisz modyfikować 20 miejsc? Testy za dużo
współdzielą (i są kruche).
y XPath? 😏
y getElementById? 😏
Albo wiem, że komponent pod spodem ma 2 IFy, nad którymi trochę móżdżyłem... i które
sprawiają że ten-trudny-kawałek-komponentu działa poprawnie. I to mnie tyle koszto-
wało glukozy, żeby te IFy poprawnie napisać, że w sumie tylko to się liczy w testach.
Przecież nic innego nie było trudne do napisania.
Sęk w tym, że jeśli fokusuję się na technikaliach i - testując - świadomie myślę o imple-
mentacji, to moje testy odzwierciedlą tą implementację. Bo SUT (system under test)
traktuję jako white box, znam go od środka. I - kiedy ta się zmieni - testy również będzie
trzeba zmienić.
Perspektywa użytkownika to to, do czego sam ma dostęp. User nie widzi propsów ani
😏
metod - a tym bardziej ręcznie ich nie wywołuje . User widzi tekst, widzi buttony
i widgety, może je kliknąć, może coś wpisać z klawiatury. To jest nasze publiczne API.
Jeśli nasz test bazuje jedynie na interakcji użytkownika i w ten sposób detereminuje
stan komponentu - to możemy potem przewrócić do góry nogami implementację 3 razy
(tylko implementację, nie funkcjonalność) - to testu ruszać nie trzeba.