Zwiększ łatwość utrzymania kodu dzięki testom integracyjnym React

Opublikowany: 2022-03-11

Testy integracyjne to idealny punkt pomiędzy kosztem a wartością testów. Pisanie testów integracyjnych dla aplikacji React za pomocą biblioteki React-testing-library zamiast lub oprócz testów jednostkowych komponentów może zwiększyć łatwość utrzymania kodu bez negatywnego wpływu na szybkość programowania.

Jeśli chcesz uzyskać przewagę, zanim przejdziemy dalej, możesz zobaczyć przykład wykorzystania biblioteki reakcji-testowania do testów integracji aplikacji React tutaj.

Dlaczego warto inwestować w testy integracyjne?

„Testy integracyjne zapewniają doskonałą równowagę między pewnością siebie a szybkością/wydatkami. Dlatego zaleca się, aby poświęcić tam większość (nie cały wysiłek)”.
– Kent C. Dodds w testach zapisu. Nie zbyt dużo. Przede wszystkim integracja.

Powszechną praktyką jest pisanie testów jednostkowych komponentów React, często przy użyciu popularnej biblioteki do testowania „enzymów” React; w szczególności jego „płytka” metoda. Takie podejście pozwala nam testować komponenty w oderwaniu od reszty aplikacji. Ponieważ jednak pisanie aplikacji React polega na komponowaniu komponentów, same testy jednostkowe nie gwarantują, że aplikacja jest wolna od błędów.

Na przykład zmiana zaakceptowanych właściwości komponentu i aktualizacja powiązanych z nim testów jednostkowych może spowodować, że wszystkie testy przejdą pomyślnie, podczas gdy aplikacja może nadal być uszkodzona, jeśli inny komponent nie został odpowiednio zaktualizowany.

Testy integracyjne mogą pomóc zachować spokój ducha podczas wprowadzania zmian w aplikacji React, ponieważ zapewniają, że kompozycja komponentów zapewnia pożądany UX.

Wymagania dotyczące testów integracji aplikacji React

Oto kilka rzeczy, które programiści React chcą robić podczas pisania testów integracyjnych:

  • Testuj przypadki użycia aplikacji z perspektywy użytkownika. Użytkownicy uzyskują dostęp do informacji na stronie internetowej i wchodzą w interakcję z dostępnymi kontrolkami.
  • Mock wywołania API, aby nie zależeć od dostępności i stanu API dla testów zaliczonych lub zakończonych niepowodzeniem.
  • Pozorne interfejsy API przeglądarki (na przykład pamięć lokalna), ponieważ po prostu nie istnieją w środowisku testowym.
  • Potwierdź stan React DOM (DOM przeglądarki lub natywne środowisko mobilne).

A teraz kilka rzeczy, których powinniśmy unikać podczas pisania testów integracji aplikacji React:

  • Testuj szczegóły implementacji. Zmiany implementacyjne powinny przerwać test tylko wtedy, gdy rzeczywiście wprowadziły błąd.
  • Za dużo kpij. Chcemy przetestować, jak wszystkie części aplikacji współpracują ze sobą.
  • Płytkie renderowanie. Chcemy przetestować skład wszystkich komponentów w aplikacji aż do najmniejszego komponentu.

Dlaczego warto wybrać bibliotekę testów React?

Wyżej wymienione wymagania sprawiają, że biblioteka testów React jest świetnym wyborem, ponieważ jej główną zasadą jest umożliwienie testowania komponentów React w sposób, który przypomina sposób, w jaki są używane przez prawdziwego człowieka.

Biblioteka, wraz z opcjonalnymi bibliotekami towarzyszącymi, pozwala nam pisać testy, które współdziałają z DOM i zapewniają jego stan.

Przykładowa konfiguracja aplikacji

Aplikacja, dla której zamierzamy napisać przykładowe testy integracyjne, realizuje prosty scenariusz:

  • Użytkownik wprowadza nazwę użytkownika GitHub.
  • Aplikacja wyświetla listę publicznych repozytoriów powiązanych z wprowadzoną nazwą użytkownika.

Sposób implementacji powyższej funkcjonalności nie powinien mieć znaczenia z perspektywy testowania integracyjnego. Jednak, aby być blisko rzeczywistych aplikacji, aplikacja podąża za typowymi wzorcami React, stąd aplikacja:

  • Jest to aplikacja jednostronicowa (SPA).
  • Wykonuje żądania API.
  • Posiada globalne zarządzanie państwem.
  • Wspiera internacjonalizację.
  • Wykorzystuje bibliotekę komponentów React.

Kod źródłowy implementacji aplikacji można znaleźć tutaj.

Pisanie testów integracyjnych

Instalowanie zależności

Z przędzą:

 yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock

Lub z npm:

 npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock

Tworzenie pliku pakietu testów integracyjnych

Stworzymy plik o nazwie viewGitHubRepositoriesByUsername.spec.js w folderze ./test naszej aplikacji. Jest automatycznie go odbierze.

Importowanie zależności w pliku testowym

 import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test

Konfigurowanie pakietu testowego

 describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });

Uwagi:

  • Przed wszystkimi testami zakpij interfejs API GitHub, aby zwrócił listę repozytoriów po wywołaniu z określoną nazwą użytkownika.
  • Po każdym teście wyczyść test React DOM, aby każdy test zaczynał się od czystego miejsca.
  • describe bloki, określ przypadek użycia testu integracji i wariacje przepływu.
  • Testowane przez nas zmiany przepływu to:
    • Użytkownik wprowadza prawidłową nazwę użytkownika, która powiązała publiczne repozytoria GitHub.
    • Użytkownik wprowadza prawidłową nazwę użytkownika, która nie ma powiązanych publicznych repozytoriów GitHub.
    • Użytkownik wprowadza nazwę użytkownika, która nie istnieje na GitHub.
  • it użycie wywołania zwrotnego asynchronicznego, ponieważ testowany przypadek użycia ma w sobie krok asynchroniczny.

Pisanie pierwszego testu przepływu

Najpierw aplikacja musi zostać wyrenderowana.

 const { getByText, getByPlaceholderText, queryByText } = render(<App />);

Metoda render zaimportowana z modułu @testing-library/react renderuje aplikację w teście React DOM i zwraca zapytania DOM powiązane z kontenerem renderowanej aplikacji. Zapytania te są używane do lokalizowania elementów DOM do interakcji i do potwierdzenia.

Teraz, jako pierwszy krok testowanego przepływu, użytkownik otrzymuje pole nazwy użytkownika i wpisuje do niego ciąg nazwy użytkownika.

 userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);

userEvent z zaimportowanego modułu @testing-library/user-event ma metodę type , która imituje zachowanie użytkownika podczas wpisywania tekstu w polu tekstowym. Przyjmuje dwa parametry: element DOM, który akceptuje dane wejściowe i ciąg znaków, który wpisuje użytkownik.

Użytkownicy zazwyczaj znajdują elementy DOM na podstawie powiązanego z nimi tekstu. W przypadku danych wejściowych jest to tekst etykiety lub tekst zastępczy. Metoda zapytania getByPlaceholderText zwrócona wcześniej z render pozwala nam znaleźć element DOM na podstawie tekstu zastępczego.

Należy pamiętać, że ponieważ sam tekst często się zmienia, najlepiej jest nie polegać na rzeczywistych wartościach lokalizacji i zamiast tego skonfigurować moduł lokalizacji tak, aby zwracał klucz pozycji lokalizacji jako swoją wartość.

Na przykład, gdy lokalizacja „en-US” normalnie zwracałaby Enter GitHub username jako wartość klucza userSelection.usernamePlaceholder , w testach chcemy, aby userSelection.usernamePlaceholder .

Gdy użytkownik wpisze tekst w polu, powinien zobaczyć zaktualizowaną wartość pola tekstowego.

 expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);

Następnie użytkownik klika przycisk przesyłania i oczekuje, że zobaczy listę repozytoriów.

 userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');

Metoda userEvent.click imituje kliknięcie przez użytkownika elementu DOM, podczas gdy zapytanie getByText znajduje element DOM na podstawie zawartego w nim tekstu. closest modyfikator gwarantuje, że dobierzemy odpowiedni element.

Uwaga: W testach integracyjnych kroki często pełnią rolę zarówno act , jak i assert . Na przykład zapewniamy, że użytkownik może kliknąć przycisk, klikając go.

W poprzednim kroku stwierdziliśmy, że użytkownik widzi sekcję listy repozytoriów aplikacji. Teraz musimy stwierdzić, że ponieważ pobieranie listy repozytoriów z GitHub może zająć trochę czasu, użytkownik widzi wskazówkę, że pobieranie jest w toku. Chcemy również upewnić się, że aplikacja nie poinformuje użytkownika, że ​​nie ma repozytoriów powiązanych z wprowadzoną nazwą użytkownika, podczas gdy lista repozytoriów jest nadal pobierana.

 getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();

Należy zauważyć, że przedrostek zapytania getBy służy do potwierdzenia, że ​​element DOM można znaleźć, a przedrostek zapytania queryBy jest przydatny w przypadku przeciwnego stwierdzenia. Ponadto queryBy nie zwraca błędu, jeśli nie zostanie znaleziony żaden element.

Następnie chcemy się upewnić, że w końcu aplikacja zakończy pobieranie repozytoriów i wyświetli je użytkownikowi.

 await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));

Metoda asynchroniczna waitForElement służy do oczekiwania na aktualizację DOM, która wygeneruje potwierdzenie dostarczone jako parametr metody true. W tym przypadku zapewniamy, że aplikacja wyświetla nazwę i opis dla każdego repozytorium zwróconego przez zafałszowany interfejs API GitHub.

Wreszcie, aplikacja nie powinna już wyświetlać wskaźnika pobierania repozytoriów i nie powinna wyświetlać komunikatu o błędzie.

 expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();

Nasz wynikowy test integracji React wygląda tak:

 it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });

Testy alternatywnego przepływu

Gdy użytkownik wprowadzi nazwę użytkownika GitHub bez powiązanych repozytoriów publicznych, aplikacja wyświetli odpowiedni komunikat.

 describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });

Gdy użytkownik wprowadzi nieistniejącą nazwę użytkownika GitHub, aplikacja wyświetli komunikat o błędzie.

 describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });

Dlaczego React testuje integrację Rock

Testowanie integracji naprawdę oferuje idealne miejsce dla aplikacji React. Testy te pomagają wyłapywać błędy i wykorzystywać podejście TDD, a jednocześnie nie wymagają konserwacji w przypadku zmian w implementacji.

React-testing-library, zaprezentowana w tym artykule, jest doskonałym narzędziem do pisania testów integracji React, ponieważ pozwala na interakcję z aplikacją tak, jak robi to użytkownik, oraz weryfikację stanu i zachowania aplikacji z perspektywy użytkownika.

Mamy nadzieję, że przedstawione tutaj przykłady pomogą Ci rozpocząć pisanie testów integracyjnych na nowych i istniejących projektach React. Pełny przykładowy kod, który zawiera implementację aplikacji, można znaleźć w moim serwisie GitHub.