Środowiska testujące

W tym rozdziale opisujemy czynniki wpływające na środowisko testujące i nasze rekomendacje dla niektórych scenariuszy.

Narzędzia uruchamiające testy (ang. test runners)

Narzędzia uruchamiające testy, jak np. Jest, mocha czy ava, pozwalają tworzyć zestawy testowe przy użyciu samego JavaScriptu, a także uruchamiać je jako część procesu tworzenia oprogramowania. Dodatkowo, testy mogą być uruchamiane w ramach procesu “ciągłej integracji” (ang. Continuous Integration, CI).

  • Jest ma wysoką kompatybilność z projektami reactowymi i obsługuje wiele przydatnych funkcjonalności, jak mockowanie modułów czy sztuczne timery. Dobrze współpracuje również z jsdom. Jeśli używasz Create React App, domyślnie masz już dostęp do Jesta z odpowiednią konfiguracją.
  • Biblioteki takie jak mocha świetnie spisują się w środowiskach przeglądarkowych, dzięki czemu mogą okazać się pomocne w przypadku niektórych testów.
  • Testy kompleksowe end-to-end, stosowane w przypadku dłuższych ścieżek rozciągających się na wiele stron aplikacji, wymagają innej konfiguracji.

Mockowanie warstwy renderującej

Testy często uruchamiane są w środowiskach niemających dostępu do prawdziwej warstwy renderującej, np. przeglądarki. Zalecamy więc symulowanie zachowań przeglądarki za pomocą jsdom, niewielkiej implementacji przeglądarki działającej na Node.js.

W większości przypadków jsdom zachowuje się jak prawdziwa przeglądarka, lecz nie posiada niektórych funkcjonalności, jak np. generowanie układu strony czy nawigacja. Mimo tego paczka z powodzeniem sprawdza się dla większości komponentów pisanych pod przeglądarkę; działa szybciej niż uruchamianie przeglądarki dla każdego testu z osobna. Ponadto, uruchamia ona testy w tym samym procesie, umożliwiając pisanie kodu sprawdzającego wyrenderowane drzewo DOM.

Podobnie jak prawdziwa przeglądarka, jsdom pozwala na modelowanie interakcji użytkownika; testy mogą wywoływać zdarzenia na węzłach DOM, a następnie obserwować i sprawdzać wyniki tych akcji (przykład).

Przy takiej konfiguracji można śmiało napisać większość testów dla UI: Jest jako narzędzie uruchamiające testy, jsdom służący do renderowania, interakcje użytkownika określone jako sekwencje zdarzeń przeglądarkowych - a to wszystko “spięte” za pomocą funkcji pomocniczej act() (przykład). Spora część testów samego Reacta jest napisana przy użyciu powyższej kombinacji.

Jeśli piszesz bibliotekę, która testuje głównie zachowania charakterystyczne dla przeglądarki, a w dodatku wymaga natywnych mechanizmów przeglądarki, jak generowanie układu strony, zalecamy skorzystanie z frameworka mocha.

W środowisku, które uniemożliwia symulowanie modelu DOM (np. podczas testowania komponentów napisanych w React Native na Node.js), możesz skorzystać z narzędzi do symulowania zdarzeń do symulowania interakcji z elementami. Alternatywnie możesz skorzystać z funkcji fireEvent dostarczonej przez @testing-library/react-native.

Frameworki jak Cypress, puppeteer czy webdriver służą do uruchamiania testów end-to-end.

Mockowanie funkcji

Podczas pisania testów czasami chcemy podmienić części naszego kodu, które nie posiadają odpowiedników w używanym przez nas środowisku (np. sprawdzanie statusu navigator.onLine w Node.js). Testy mogą również śledzić niektóre funkcje i obserwować, jak pozostałe części kodu wchodzą z nimi w interakcje. Pomocna okazuje się wtedy możliwość wybiórczego zastąpienia niektórych funkcji wersjami odpowiednimi dla testów.

Szczególnie przydatne okazuje się to przy pobieraniu danych. Zazwyczaj lepiej w testach używać “sztucznych” danych, aby uniknąć spowolnień czy niestabilności z powodu odwołań do prawdziwego API (przykład). Dzięki takiemu zabiegowi testy są przewidywalne. Biblioteki typu Jest czy sinon wspierają mockowanie funkcji. W przypadku testów end-to-end, mockowanie sieci może okazać się trudniejsze, choć należy tego unikać i zamiast tego testować korzystając z prawdziwego API.

Mockowanie modułów

Niektóre komponenty mają zależności w modułach, które mogą nie działać w środowisku testowym lub które zwyczajnie nie są istotne z punktu widzenia naszych testów. Warto wtedy zastąpić te moduły czymś odpowiednim dla danego przypadku (przykład).

W Node.js mockowanie modułów jest wspierane np. przez bibliotekę Jest. Można to również osiągnąć z pomocą paczki mock-require.

Mockowanie timerów

Komponenty mogą korzystać z funkcji opartych na czasie, np. setTimeout, setInterval czy Date.now. W środowisku testowym warto zamieniać tego typu funkcje na ich zastępniki, które pozwalają ręcznie “sterować czasem”. Testy korzystające z timerów nadal będą wykonywać się w odpowiedniej kolejności, ale zdecydowanie szybciej (przykład). Większość frameworków, również Jest, sinon oraz lolex, pozwalają na mockowanie timerów w testach.

Niekiedy jednak możesz chcieć skorzystać z prawdziwych timerów, na przykład, gdy testujesz animację lub interakcję z endpointem, który zależy od czasu (np. ogranicza częstość odpytywania API). Biblioteki zawierające sztuczne timery pozwalają na łatwe włączanie i wyłączanie tego mechanizmu dla każdego zestawu testowego lub pojedynczego testu. Dzięki temu możesz zdecydować, jak poszczególne testy mają być uruchamiane.

Testy end-to-end

Testy end-to-end są efektywne przy testowaniu dłuższych sekwencji interakcji, zwłaszcza jeśli są one krytyczne dla twojego produktu (np. płatność czy rejestracja). W takich przypadkach konieczne jest przetestowanie, jak przeglądarka renderuje całą aplikację, jak pobiera dane z API, korzysta z sesji i ciasteczek lub nawiguje pomiędzy poszczególnymi stronami. Możesz w nich sprawdzać nie tylko stan drzewa DOM, lecz także sterujące nim dane (np. weryfikując, czy dane zostały zapisane w bazie danych).

Do takich scenariuszy możesz skorzystać z frameworka Cypress lub biblioteki puppeteer, które pozwalają nawigować pomiędzy stronami i sprawdzać rezultaty nie tylko w samej przeglądarce, ale potencjalnie również na backendzie.