Piątka ludzi dająca sobie żółwika nad biurkiem z sprzętem elektronicznym

Jak pisać testy integracyjne komponentów React z Jest i Enzyme

Podczas testowania interfejsu użytkownika ograniczanie się sprawdzania odizolowanych od siebie komponentów nie zdaje egzaminu. Funkcjonalności dostarczane przez aplikację zawsze są wypadkową właściwej współpracy kilku jednostek w środowisku przeglądarki. Dopiero kiedy upewnisz się, że podsystemy są właściwie ze sobą zintegrowane, znacznie wzrośnie prawdopodobieństwo, że aplikacja spełni swoje zadanie w akcji. Dzisiaj przeprowadzę Cię przez podstawowy proces planowania i implementacji testów integracyjnych komponentów React w środowisku Jest i Enzyme.

Swoją drogą, istnieje liczna grupa programistów, z Kentem C. Doddsem na czele, która twierdzi, że pisanie dużej liczby testów jednostkowych jest mniej efektywne niż kilka solidnych testów integracyjnych. Efektem takiej filozofii jest zastąpienie klasycznej piramidy testowania takim oto osobliwym kształtem:
Model testowania z naciskiem na testy integracyjne autorstwa Kenta C. Doddsa

Czy taki model faktycznie lepiej sprawdzi się w każdym projekcie? Najzwyczajniej brak mi doświadczenia, żeby udzielić zdecydowanej odpowiedzi. Argumenty Kenta przemawiają do mnie na tyle, że na pewno sprawdzę skuteczność jego podejścia w praktyce.

Niezależnie od całej dyskusji, warto sprawnie radzić sobie zarówno z pisaniem testów jednostkowych, jak i integracyjnych. Ten wpis jest bezpośrednią kontynuacją poprzedniego: Jak pisać testy jednostkowe komponentów w React z Jest i Enzyme. Będę nawiązywał do pojęć, które wyjaśniłem ostatnio, więc jeżeli masz zaległości, zachęcam do ich nadrobienia :).

Jak zorganizować dobrą integrację?

Tak jak mieliśmy do czynienia z kontraktem pojedynczego komponentu, tak możemy wyznaczyć kontrakt podsystemu, który poddamy testom integracyjnym.

Jak wyznaczyć granice podsystemu? Na ogół jego korzeniem jest komponent kontenerowy. W przypadku tak małych projektów jak crypto-tracker, mamy tak naprawdę jeden podsystem, który składa się na całą aplikację. Naszym korzeniem jest komponent App (kod źródłowy znajdziesz tutaj). Dzięki przeprowadzonym wcześniej testom jednostkowym wiemy, że wypełnia swoje zadania w izolacji. Teraz pozostało sprawdzić, jak radzi sobie w zarządzaniu współpracą renderowanych przez niego dzieci: SearchBar i CoinList.

Większość informacji o tym, jak przebiega wspomniana kooperacja, dostarczy nam analiza kodu źródłowego oraz testów jednostkowych.

Umożliwianie współpracy pomiędzy rodzicem a jego dzieckiem to główna odpowiedzialność props, więc właśnie ten obiekt dostarczy nam dużo użytecznych informacji o tym co wymaga przetestowania. W testach jednostkowych mamy informacje co do tego, jakie propsy komponent otrzymuje od swoich przodków oraz co przekazuje swoim dzieciom. Jeżeli tych testów jednostkowych nie ma w naszym projekcie zbyt wiele, to najlepiej przejść przez proces wyznaczania kontraktu każdego komponentu wchodzącego w skład podsystemu. Łatwo będzie wychwycić punkty wspólne dla kilku komponentów, czyli naszą tytułową integrację.

Nie zaszkodzi również odpalić demo aplikacji oraz spojrzeć na nią z punktu widzenia użytkownika. To pozwoli nam nabrać większego dystansu do kodu, co pozytywnie wpłynie na implementację samych przypadków testowych. W testach integracyjnych warto odejść od wewnętrznej logiki komponentów na rzecz efektów końcowych ich pracy: czy (a nie jak) osiągnęliśmy oczekiwane rezultaty przy wyświetlaniu zawartości bądź obsłudze zdarzenia?

Zastosowanie powyższych wskazówek pozwoliło mi wypisać następujące wymagania co do crypto-trackera:

  • Aplikacja wyświetla animację ładowania, dopóki dane nie zostaną pobrane z serwera
  • Aplikacja wyświetla listę kryptowalut pobranych z serwera po ich załadowaniu
  • Aplikacja pozwala na przeszukiwanie listy za pomocą paska wyszukiwania

Okej, skoro już wiemy co musimy przetestować, to czas ubrudzić sobie ręce. Wracamy do Jest i Enzyme.

Dziel i rządź

Zanim zabierzemy się za implementacje przypadków testowych, rozdzielimy testy jednostkowe i integracyjne na osobne pliki. Taka separacja otworzy nam furtkę do uruchamiania wyłącznie wybranego rodzaju testów. Przy fixowaniu buga ograniczenie się do wielokrotnego wykonywania testów jednostkowych zaoszczędzi nam dużo cennego czasu. Często testy integracyjne zostawia się na koniec, gdy wszystko śmiga już w odizolowanym środowisku.

Zacznijmy od stworzenia folderu src/containers/tests, przenieśmy do niego App.test.js i zmieńmy jego nazwę na App.unit.test.js. Następnie utwórzmy plik przeznaczony do testów integracyjnych: App.int.test.js.

To nie wszystko, musimy jeszcze zmodyfikować npm scripts w package.json. Przyda nam się argument konsolowy Jest: --testPathPattern:

Jak widzisz, mamy również do dyspozycji skrypt test, który pozwoli na odpalenie wszystkich testów. Jesteśmy gotowi na każdą ewentualność.

Przed wyruszeniem w drogę należy… przygotować rusztowanie

Klasycznie czeka nas jeszcze jeden przystanek przed prawdziwym testerskim szaleństwem. Musimy przygotować funkcje pomocnicze, hooki i inne tego typu historie.

Tym razem będziemy korzystali z mount renderingu. Do sprawdzenia współpracy App i jego podopiecznych, musimy wyrenderować całe drzewo tego komponentu. Shallow rendering, wykorzystywany w testach jednostkowych, nie zdałby tutaj egzaminu, bo zwraca jedynie płytką wersję root komponentu.

Praca zespołowa pod lupą

Jesteśmy gotowi na implementację testów, które zaplanowaliśmy. Do dzieła.

  • Aplikacja wyświetla animacje ładowania, dopóki dane nie zostaną pobrane z serwera.

Tutaj wystarczą dwie asercje. Pierwsza sprawdzi, czy komponent obsługujący animację ładowania Spinner został wyrenderowany. Druga upewni nas, że lista nie wyświetla jakiegokolwiek Coina.

  • Aplikacja wyświetla listę kryptowalut pobranych z serwera po ich załadowaniu

Zacznijmy od upewnienia się, że Spinner znika po załadowaniu danych.

Mimo że kod testu sprawia wrażenie poprawnego, wysypuje się, twierdząc, że Spinner nadal jest wyświetlany przez CoinList.

Na początku byłem przekonany, że wynika to z błędu w logice samej aplikacji. W końcu od tego są testy. Jednak szybka randka z debuggerem (oprócz niesamowitych wspomnień) doprowadziła mnie do innych wniosków. Wszystko działa jak należy. Zabrałem się za debugowanie samych testów: stan appWrappera w momencie wykonywania asercji jasno wskazywał, że state.cryptos ma przypisane zmockowane dane, a isLoading jest ustawione na false.

Teraz największy hit, który wprowadził mnie w stan absolutnego zdezorientowania.

W ramach dalszego śledztwa postanowiłem porzucić wyszukane metody. Pozostała mi ostatnia deska ratunku, najpopularniejszy sposób na uzyskanie odpowiedzi na pytanie „co się tutaj od.. dzieje?” w ekosystemie JS. W ruch poszedł console.log().

Z perskeptywy czasu, nie wiem, czy to był dobry pomysł. Zgadnijcie, co zwrócił console.log(coinListWrapper.text()) – tekst wygenerowany na podstawie zmockowanych kryptowalut i ani śladu po Spinnerze.

Zrozumiałem, co się wyprawia (częściowo) dopiero po przeczytaniu tego wątku na GitHubie.

W skrócie: wrappery dzieci są „odcięte” od rodzica. Zmiany w rodzicu nie są automatycznie odzwierciedlane w wrapperze dziecka. coinListWrapper to referencja do wrappera sprzed wywołania appWrapper.update().

Byłoby to całkiem sensowne, tylko skąd taki wynik wywołania w coinListWrapper.text()>, który jednak oddaje aktualny stan rodzica. Tego nie było dane mi pojąć. Jeżeli znasz rozwiązanie tej zagadki, koniecznie daj znać w komentarzach :).

Aby uniknąć całego tego zamieszania, musimy inaczej zabrać się za uzyskiwanie dostępu do CoinList. Jak już ustaliliśmy, w asercji interesuje nas aktualny stan wrappera, a nie studiowanie zaburzeń czasoprzestrzeni. Stąd zamiast zmiennej, przyda nam się funkcja, która będzie zwracała świeżo odszukany wrapper.

Od tej pory odwołuję się do CoinList za pomocą wywołań getCoinListWrapper().

Okej, teraz jest zielono, kryzys zażegnany.

Przejdźmy do sprawdzenia, czy liczebność Coinów pokrywa się z liczebnością zmockowanych danych.

Kolejny punkt na naszej liście odhaczony.

  • Aplikacja pozwala na przeszukiwanie listy za pomocą paska wyszukiwania

Powyższe wymaganie rozbijemy na kilka przypadków testowych.

  • Aplikacja dopasowuje do frazy wyszukiwania tylko jedną walutę

  • Aplikacja dopasowuje do frazy wyszukiwania kilka walut

  • Aplikacja wyświetla wiadomość informującą o braku wyników przy braku dopasowań

  • Aplikacja wyświetla wszystkie dostepne dane po usunięciu frazy wyszukiwania

I to by było na tyle. Cały kod testów integracyjnych znajdziecie tutaj.

Podsumowanie

Gdyby nie ta zagadka rodem z Sherlocka Holmesa, to testy integracyjne poszłyby naprawdę gładko. Umiejętności, które zdobyliśmy przy pisaniu testów jednostkowych oraz znajomość API Enzyme, mocno zaprocentowało przy testach integracyjnych.

Najważniejsze sprawy, o których powinieneś pamiętać:

  • Testy integracyjne nie muszą być liczne, aby dostarczyć dużo wartości
  • Pisz funkcje pomocnicze odszukujące wrappery dzieci, zaoszczędzisz sobie nieprzyjemnych niespodzianek

Przed nami ostatni przystanek na testerskiej wyprawie: testy end-to-end. Porzucamy Enzyme i Jest na rzecz cypress. Zmieni się forma i narzędzie, ale wszystko sprowadzi się do odtworzenia w przeglądarce scenariusza zbudowanego na podstawie przeprowadzonych dzisiaj testów integracyjnych.

Podobał Ci się dzisiejszy artykuł? Udostępnij go w Twoich ulubionych mediach społecznościowych. Może znajdzie się ktoś, dla kogo również będzie wartościowy.

Bądź na bieżąco. Wystarczy polubić fanpage AlgoSmart na fejsie, obserwować mój profil na Twitterze i regularnie odwiedzać portal Polski Front-end.

Zdjęcie tytułowe autorstwa: unsplash-logorawpixel

Marcin Czarkowski

Cześć! Ja nazywam się Marcin Czarkowski, a to jest AlgoSmart - blog, na którym dzielę się wiedzą o React.js, JavaScript oraz CSS.