Przejdź do treści głównej
Wersja: 0.82

Testowanie

Nieoficjalne Tłumaczenie Beta

Ta strona została przetłumaczona przez PageTurner AI (beta). Nie jest oficjalnie zatwierdzona przez projekt. Znalazłeś błąd? Zgłoś problem →

Wraz z rozrostem bazy kodu, drobne błędy i nieoczekiwane przypadki brzegowe mogą prowadzić do poważniejszych awarii. Błędy skutkują słabym doświadczeniem użytkownika, a w konsekwencji stratami biznesowymi. Jednym ze sposobów zapobiegania kruchej implementacji jest testowanie kodu przed udostępnieniem go na produkcji.

W tym przewodniku omówimy różne zautomatyzowane metody zapewniające poprawne działanie aplikacji – od analizy statycznej po testy end-to-end.

Testing is a cycle of fixing, testing, and either passing to release or failing back into testing.

Dlaczego testować

Jesteśmy ludźmi, a ludzie popełniają błędy. Testowanie jest ważne, ponieważ pomaga wykryć te błędy i zweryfikować działanie kodu. Co istotniejsze, testowanie zapewnia, że kod będzie działał poprawnie w przyszłości podczas dodawania nowych funkcji, refaktoryzacji istniejących lub aktualizacji głównych zależności projektu.

Testowanie ma większą wartość, niż mogłoby się wydawać. Jednym z najlepszych sposobów naprawy błędu jest napisanie testu, który go ujawni. Po naprawieniu błędu i ponownym uruchomieniu testu, jeśli przejdzie on pomyślnie, oznacza to, że błąd został naprawiony i nie powróci do kodu.

Testy mogą również służyć jako dokumentacja dla nowych członków zespołu. Osobom, które pierwszy raz widzą bazę kodu, czytanie testów pomaga zrozumieć działanie istniejącego kodu.

Last but not least, more automated testing means less time spent with manual QA, freeing up valuable time.

Analiza statyczna

Pierwszym krokiem do poprawy jakości kodu jest wykorzystanie narzędzi do analizy statycznej. Analiza statyczna sprawdza kod pod kątem błędów podczas jego pisania, bez uruchamiania.

  • Lintery analizują kod w poszukiwaniu typowych błędów, takich jak nieużywany kod, pomagają unikać pułapek oraz flagują naruszenia stylu kodu (np. użycie tabulatorów zamiast spacji lub odwrotnie, w zależności od konfiguracji).

  • Sprawdzanie typów zapewnia, że konstrukcje przekazywane do funkcji pasują do oczekiwań (np. zapobiega przekazaniu ciągu znaków do funkcji liczącej oczekującej liczby).

React Native zawiera domyślnie dwa takie narzędzia: ESLint do lintowania oraz TypeScript do sprawdzania typów.

Pisanie testowalnego kodu

Aby zacząć testować, najpierw potrzebujesz kodu, który da się testować. Pomyśl o procesie produkcji samolotów – zanim model wzleci, by pokazać, że wszystkie systemy współdziałają, poszczególne części są testowane pod kątem bezpieczeństwa i funkcjonalności. Skrzydła testuje się pod ekstremalnym obciążeniem, części silnika pod kątem wytrzymałości, a przednią szybę pod kątem uderzenia ptaka.

Podobnie jest z oprogramowaniem. Zamiast pisać cały program w jednym ogromnym pliku, podziel kod na mniejsze moduły, które można dokładniej przetestować niż całość. Pisanie testowalnego kodu łączy się z pisaniem czystego, modularnego kodu.

Aby aplikacja była bardziej testowalna, oddziel warstwę widoku (komponenty React) od logiki biznesowej i stanu aplikacji (niezależnie od użycia Reduxa, MobX czy innych rozwiązań). Dzięki temu testowanie logiki biznesowej – które nie powinno zależeć od komponentów React – będzie niezależne od komponentów, których głównym zadaniem jest renderowanie UI!

Teoretycznie można nawet przenieść całą logikę i pobieranie danych poza komponenty. Wtedy komponenty skupiają się wyłącznie na renderowaniu, stan jest całkowicie od nich niezależny, a logika aplikacji działałaby bez żadnych komponentów React!

wskazówka

Zachęcamy do głębszego zgłębienia tematu testowalnego kodu w innych materiałach.

Pisanie testów

Po napisaniu testowalnego kodu, czas na pisanie prawdziwych testów! Domyślny szablon React Native zawiera framework testowy Jest. Jest on wyposażony w gotową konfigurację dostosowaną do tego środowiska, dzięki czemu możesz od razu zacząć pracować bez konieczności konfigurowania i tworzenia atrap — więcej o atrapach za chwilę. Możesz użyć Jesta do pisania wszystkich typów testów opisanych w tym przewodniku.

uwaga

Jeśli stosujesz programowanie sterowane testami (TDD), to w rzeczywistości najpierw piszesz testy! Dzięki temu testowalność twojego kodu jest zapewniona.

Struktura testów

Twoje testy powinny być krótkie i najlepiej sprawdzać tylko jedną rzecz. Zacznijmy od przykładowego testu jednostkowego napisanego w Jest:

js
it('given a date in the past, colorForDueDate() returns red', () => {
expect(colorForDueDate('2000-10-20')).toBe('red');
});

Test jest opisany ciągiem znaków przekazanym do funkcji it. Przywiązaj dużą wagę do opisu, aby było jasne, co jest testowane. Postaraj się uwzględnić:

  1. Given (Warunki) - określone warunki wstępne

  2. When (Akcja) - działanie wykonywane przez testowaną funkcję

  3. Then (Rezultat) - oczekiwany wynik

Ten schemat jest również znany jako AAA (Arrange, Act, Assert).

Jest oferuje funkcję describe do grupowania testów. Użyj describe, aby zgrupować wszystkie testy dotyczące jednej funkcjonalności. Grupy mogą być zagnieżdżone. Inne przydatne funkcje to beforeEach i beforeAll, które służą do konfigurowania testowanych obiektów. Więcej w dokumentacji API Jesta.

Jeśli test ma wiele kroków lub asercji, warto podzielić go na mniejsze fragmenty. Upewnij się też, że testy są całkowicie niezależne. Każdy test w zestawie musi działać samodzielnie bez uruchamiania innych testów. Jednocześnie, gdy uruchamiasz wszystkie testy razem, pierwszy test nie może wpływać na wyniki następnego.

Pamiętaj: jako programiści lubimy, gdy kod działa idealnie. W testach często jest odwrotnie. Traktuj niezaliczony test jako coś dobrego! Gdy test nie przechodzi, często oznacza to problem. Daje ci to szansę na naprawę, zanim użytkownicy go napotkają.

Testy jednostkowe

Testy jednostkowe sprawdzają najmniejsze fragmenty kodu, jak pojedyncze funkcje lub klasy.

Gdy testowany obiekt ma zależności, często trzeba je zastąpić atrapami, jak opisano poniżej.

Zaletą testów jednostkowych jest szybkość pisania i uruchamiania. Dzięki temu podczas pracy otrzymujesz natychmiastową informację o wynikach testów. Jest nawet oferuje opcję ciągłego uruchamiania testów powiązanych z edytowanym kodem: Tryb watch.

Tworzenie atrap (Mocking)

Czasem, gdy testowane obiekty mają zewnętrzne zależności, będziesz chciał je "zastąpić atrapami". "Mockowanie" oznacza zastąpienie zależności twojego kodu własną implementacją.

informacja

Ogólnie lepiej używać prawdziwych obiektów niż atrap, ale są sytuacje, gdy to niemożliwe. Np. gdy test jednostkowy w JS zależy od natywnego modułu w Javie lub Objective-C.

Wyobraź sobie aplikację pokazującą pogodę w twoim mieście, która używa zewnętrznej usługi dostarczającej dane pogodowe. Gdy usługa zgłasza deszcz, chcesz pokazać obrazek z deszczową chmurą. Nie chcesz jednak wywoływać tej usługi w testach, ponieważ:

  • Mogłoby to spowolnić testy i uczynić je niestabilnymi (z powodu żądań sieciowych)

  • Usługa może zwracać różne dane przy każdym uruchomieniu testu

  • Usługi stron trzecich mogą przestać działać akurat wtedy, gdy potrzebujesz uruchomić testy!

Dlatego możesz dostarczyć atrapę implementacji takiej usługi, skutecznie zastępując tysiące linii kodu i termometry podłączone do internetu!

uwaga

Jest oferuje wsparcie dla tworzenia atrap od poziomu funkcji aż do poziomu modułów.

Testy integracyjne

Podczas tworzenia większych systemów oprogramowania, ich poszczególne części muszą ze sobą współdziałać. W testach jednostkowych, jeśli twoja jednostka zależy od innej, czasem skończysz na tworzeniu atrapy tej zależności, zastępując ją fałszywym odpowiednikiem.

W testach integracyjnych rzeczywiste pojedyncze jednostki są łączone (tak jak w twojej aplikacji) i testowane razem, aby zapewnić, że ich współpraca działa zgodnie z oczekiwaniami. Nie oznacza to, że atrapy nie są tu używane: nadal będziesz potrzebować atrap (np. do zasymulowania komunikacji z usługą pogodową), ale w znacznie mniejszym zakresie niż w testach jednostkowych.

informacja

Pamiętaj, że terminologia dotycząca testów integracyjnych nie zawsze jest spójna. Również granica między testem jednostkowym a integracyjnym nie zawsze jest jasna. W tym przewodniku, twój test zalicza się do "testów integracyjnych", jeśli:

  • Łączy kilka modułów twojej aplikacji, jak opisano powyżej
  • Korzysta z systemu zewnętrznego
  • Wykonuje wywołanie sieciowe do innej aplikacji (np. API usługi pogodowej)
  • Wykonuje operacje na plikach lub bazie danych (I/O)

Testy komponentów

Komponenty React są odpowiedzialne za renderowanie twojej aplikacji, a użytkownicy bezpośrednio wchodzą w interakcję z ich wynikiem. Nawet jeśli logika biznesowa twojej aplikacji ma wysokie pokrycie testami i jest poprawna, bez testów komponentów możesz dostarczyć użytkownikom uszkodzony interfejs. Testy komponentów mogą zaliczać się zarówno do testów jednostkowych, jak i integracyjnych, ale ponieważ są kluczową częścią React Native, omówimy je osobno.

Podczas testowania komponentów React możesz skupić się na dwóch obszarach:

  • Interakcje: zapewnienie poprawnego działania komponentu podczas interakcji z użytkownikiem (np. naciśnięcie przycisku)

  • Renderowanie: zapewnienie poprawności wyniku renderowania komponentu używanego przez React (np. wygląd przycisku i jego umiejscowienie w UI)

Na przykład, jeśli masz przycisk z nasłuchiwaczem onPress, chcesz przetestować zarówno poprawne wyświetlanie przycisku, jak i poprawne obsługiwanie jego naciśnięcia przez komponent.

Do testowania tych obszarów możesz wykorzystać kilka bibliotek:

  • React Native Testing Library bazuje na rendererze testowym Reacta i dodaje API fireEvent oraz query, opisane w następnym akapicie.

  • [Przestarzałe] Test Renderer Reacta, rozwijany równolegle z jego rdzeniem, umożliwia renderowanie komponentów do czystych obiektów JavaScript bez zależności od DOM czy natywnego środowiska mobilnego.

ostrzeżenie

Testy komponentów to tylko testy JavaScript działające w środowisku Node.js. Nie uwzględniają one kodu iOS, Androida ani innych platform wspierających komponenty React Native. W związku z tym nie dają 100% pewności, że wszystko działa dla użytkownika. Jeśli błąd występuje w kodzie iOS lub Androida, nie zostanie wykryty.

Testowanie interakcji użytkownika

Poza renderowaniem interfejsu, twoje komponenty obsługują zdarzenia jak onChangeText dla TextInput czy onPress dla Button. Mogą też zawierać inne funkcje i wywołania zwrotne zdarzeń. Rozważ następujący przykład:

tsx
function GroceryShoppingList() {
const [groceryItem, setGroceryItem] = useState('');
const [items, setItems] = useState<string[]>([]);

const addNewItemToShoppingList = useCallback(() => {
setItems([groceryItem, ...items]);
setGroceryItem('');
}, [groceryItem, items]);

return (
<>
<TextInput
value={groceryItem}
placeholder="Enter grocery item"
onChangeText={text => setGroceryItem(text)}
/>
<Button
title="Add the item to list"
onPress={addNewItemToShoppingList}
/>
{items.map(item => (
<Text key={item}>{item}</Text>
))}
</>
);
}

Podczas testowania interakcji, testuj komponent z perspektywy użytkownika — co jest widoczne? Co zmienia się po interakcji?

Zasadniczo, preferuj elementy, które użytkownik może zobaczyć lub usłyszeć:

Z drugiej strony, powinieneś unikać:

  • wykonywania asercji na właściwościach (props) lub stanie komponentu

  • zapytań testID

Unikaj testowania szczegółów implementacyjnych jak właściwości czy stan — choć takie testy działają, nie skupiają się na interakcjach użytkowników z komponentem i często ulegają złamaniu podczas refaktoryzacji (np. przy zmianie nazw lub przejściu z komponentów klasowych na hooki).

informacja

Komponenty klasowe React są szczególnie podatne na testowanie ich wewnętrznych szczegółów implementacyjnych, takich jak stan wewnętrzny, właściwości czy procedury obsługi zdarzeń. Aby uniknąć testowania szczegółów implementacyjnych, preferuj używanie komponentów funkcyjnych z Hooks, co utrudnia poleganie na wewnętrznych elementach komponentu.

Biblioteki do testowania komponentów, takie jak React Native Testing Library, ułatwiają pisanie testów zorientowanych na użytkownika poprzez staranny dobór udostępnianych API. W poniższym przykładzie wykorzystano metody fireEvent: changeText i press, które symulują interakcję użytkownika z komponentem, oraz funkcję zapytań getAllByText, która znajduje pasujące węzły Text w renderowanym wyniku.

tsx
test('given empty GroceryShoppingList, user can add an item to it', () => {
const {getByPlaceholderText, getByText, getAllByText} = render(
<GroceryShoppingList />,
);

fireEvent.changeText(
getByPlaceholderText('Enter grocery item'),
'banana',
);
fireEvent.press(getByText('Add the item to list'));

const bananaElements = getAllByText('banana');
expect(bananaElements).toHaveLength(1); // expect 'banana' to be on the list
});

Ten przykład nie testuje zmian stanu po wywołaniu funkcji. Testuje on, co się dzieje, gdy użytkownik zmienia tekst w TextInput i naciska przycisk Button!

Testowanie renderowanego wyniku

Testowanie migawkowe (snapshot testing) to zaawansowana technika testowania udostępniana przez Jesta. Jest to bardzo potężne, niskopoziomowe narzędzie, dlatego zaleca się szczególną ostrożność przy jego stosowaniu.

"Migawka komponentu" to ciąg podobny do JSX, generowany przez niestandardowy serializator Reacta wbudowany w Jesta. Serializator ten pozwala Jesta przekształcić drzewo komponentów Reacta w czytelny dla człowieka tekst. Innymi słowy: migawka komponentu to tekstowa reprezentacja renderowanego wyniku twojego komponentu wygenerowana podczas przebiegu testu. Może wyglądać następująco:

tsx
<Text
style={
Object {
"fontSize": 20,
"textAlign": "center",
}
}>
Welcome to React Native!
</Text>

W testowaniu migawkowym zwykle najpierw implementujesz komponent, a następnie uruchamiasz test migawkowy. Test ten tworzy migawkę i zapisuje ją w repozytorium jako migawkę referencyjną. Plik jest następnie commitowany i sprawdzany podczas code review. Wszelkie przyszłe zmiany w renderowanym wyniku komponentu zmienią jego migawkę, co spowoduje niepowodzenie testu. Wtedy musisz zaktualizować przechowywaną migawkę referencyjną, aby test przeszedł. Ta zmiana ponownie wymaga commitowania i przeglądu.

Migawki mają kilka słabych punktów:

  • Jako programista lub recenzent możesz mieć trudności z oceną, czy zmiana w migawce jest zamierzona, czy świadczy o błędzie. Szczególnie duże migawki szybko stają się trudne do zrozumienia, a ich wartość dodana maleje.

  • W momencie tworzenia migawka jest uznawana za poprawną — nawet jeśli renderowany wynik jest błędny.

  • Gdy migawka nie przejdzie, istnieje pokusa jej aktualizacji za pomocą opcji Jesta --updateSnapshot bez należytego sprawdzenia, czy zmiana jest oczekiwana. Wymaga to dyscypliny ze strony programisty.

Migawki same w sobie nie gwarantują poprawności logiki renderowania komponentu. Są głównie skuteczne w zabezpieczaniu przed nieoczekiwanymi zmianami i weryfikowaniu, czy komponenty w testowanym drzewie Reacta otrzymują oczekiwane właściwości (style itp.).

Zalecamy używanie wyłącznie małych migawek (patrz: reguła no-large-snapshots). Jeśli chcesz przetestować różnicę między dwoma stanami komponentu Reacta, użyj snapshot-diff. W razie wątpliwości preferuj jawne asercje, jak opisano w poprzednim akapicie.

Testy End-to-End

W testach end-to-end (E2E) weryfikujesz, czy twoja aplikacja działa zgodnie z oczekiwaniami na urządzeniu (lub symulatorze/emulatorze) z perspektywy użytkownika.

Osiąga się to poprzez zbudowanie aplikacji w konfiguracji produkcyjnej i uruchomienie testów na tej wersji. W testach E2E nie myślimy już o komponentach Reacta, API React Native, magazynach Reduxa ani żadnej logice biznesowej. To nie jest cel testów E2E, a te elementy nie są nawet dostępne podczas testowania E2E.

Zamiast tego biblioteki do testów E2E umożliwiają znajdowanie i kontrolowanie elementów na ekranie aplikacji: na przykład możesz faktycznie naciskać przyciski lub wprowadzać tekst do TextInputs w taki sam sposób, jak robiłby to prawdziwy użytkownik. Następnie możesz sprawdzać, czy dany element istnieje na ekranie aplikacji, czy jest widoczny, jaki tekst zawiera itp.

Testy E2E dają najwyższy poziom pewności, że część twojej aplikacji działa prawidłowo. Kompromisy obejmują:

  • ich pisanie zajmuje więcej czasu w porównaniu z innymi rodzajami testów

  • są wolniejsze w wykonaniu

  • są bardziej podatne na flakiness („flaky test” to test, który losowo przechodzi lub nie przechodzi bez zmian w kodzie)

Postaraj się objąć testami E2E kluczowe części aplikacji: przepływ uwierzytelniania, podstawowe funkcjonalności, płatności itp. Do mniej istotnych części użyj szybszych testów JS. Im więcej testów dodasz, tym większą masz pewność działania, ale też więcej czasu poświęcisz na ich utrzymanie i uruchamianie. Rozważ kompromisy i zdecyduj, co będzie dla ciebie najlepsze.

Dostępnych jest kilka narzędzi do testów E2E: w społeczności React Native popularnym frameworkiem jest Detox, ponieważ jest dostosowany do aplikacji React Native. Inną popularną biblioteką dla aplikacji iOS i Android jest Appium lub Maestro.

Podsumowanie

Mamy nadzieję, że lektura tego przewodnika była przyjemna i czegoś się nauczyłeś. Istnieje wiele sposobów testowania aplikacji. Początkowo wybór może być trudny, ale wierzymy, że wszystko stanie się jasne, gdy zaczniesz dodawać testy do swojej świetnej aplikacji React Native. Więc na co czekasz? Zwiększaj swoje pokrycie testami!

Linki


Ten przewodnik został w całości napisany i opracowany przez Vojtecha Novaka.