Testowanie komponentów w praktyce

Zademonstruj praktyczny kod testowy, czyli testy składowe. Testy składowe są ważniejsze niż proste testy jednostkowe, są mniej złożone niż kompleksowe testy oraz wykazują interakcję z modelem DOM. Według filozofii zastosowanie React ułatwia programistom myślenie o witrynach i aplikacjach internetowych jako złożonych z komponentów.

Testowanie poszczególnych komponentów, niezależnie od ich złożoności, to dobry sposób na rozpoczęcie testowania nowej lub istniejącej aplikacji.

Na tej stronie omówiono testowanie niewielkiego komponentu ze złożonymi zależnościami zewnętrznymi. Można łatwo przetestować komponent, który nie wchodzi w interakcję z innym kodem, na przykład klikając przycisk i potwierdzając, że dana liczba się zwiększa. W rzeczywistości niewiele jest takich kodów, a testowanie kodu niezawierającego interakcji może mieć ograniczoną wartość.

(Nie jest to pełny samouczek. W kolejnej sekcji, w ramach praktycznego testowania, omówimy testowanie prawdziwej witryny z przykładowym kodem do wykorzystania jako samouczek. ale ta strona nadal zawiera kilka przykładów praktycznych testów elementów).

Testowany komponent

Do testowania komponentu React użyjemy Vitest i jego środowiska JSDOM. Dzięki temu będziemy mogli szybko przeprowadzać testy za pomocą Node w wierszu poleceń podczas emulacji przeglądarki.

Lista nazw z przyciskiem Wybierz obok każdej z nich.
Niewielki komponent React, który wyświetla listę użytkowników z sieci.

Komponent Reakcja o nazwie UserList pobiera listę użytkowników z sieci i umożliwia wybranie jednego z nich. Listę użytkowników uzyskuje się za pomocą funkcji fetch w elemencie useEffect, a moduł wyboru jest przekazywany przez Context. Oto jego kod:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

Ten przykład nie pokazuje sprawdzonych metod React (np. kod fetch w useEffect), ale Twoja baza kodu prawdopodobnie zawiera wiele podobnych przypadków. Co więcej, takie przypadki mogą się wydawać trudniejsze do sprawdzenia na pierwszy rzut oka. W przyszłej części tego kursu powiemy szczegółowo o tworzeniu kodu, który można przetestować.

Oto rzeczy, które testujemy w tym przykładzie:

  • Sprawdź, czy w odpowiedzi na dane z sieci powstaje pewien prawidłowy model DOM.
  • Potwierdź, że kliknięcie użytkownika wywołuje wywołanie zwrotne.

Każdy komponent jest inny. Co sprawia, że testowanie jest interesujące?

  • Wykorzystuje globalny fetch do żądania rzeczywistych danych z sieci, które w testach mogą być niestabilne lub powolne.
  • Importuje inną klasę, UserRow, której możemy nie chcieć testować.
  • Używa w nim elementu Context, który nie jest częścią testowanego kodu i jest zwykle udostępniany przez komponent nadrzędny.

Na początek napisz krótki test

Możemy szybko przetestować coś bardzo prostego w tym komponencie. Dla jasności – ten przykład nie jest zbyt przydatny. Warto jednak skonfigurować gotowy plik w pliku równorzędnym o nazwie UserList.test.tsx. Pamiętaj, że aplikacje z eksperymentów, tacy jak Vitest, domyślnie będą uruchamiać pliki z końcówką .test.js lub podobną, w tym .tsx:

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

Ten test potwierdza, że podczas renderowania komponentu zawiera on tekst „Użytkownicy”. Działa on mimo tego, że efekt uboczny wysyłania komponentu fetch do sieci jest aktywny. fetch jest nadal wykonywany na końcu testu bez ustawionego punktu końcowego. Nie możemy potwierdzić, czy po zakończeniu testu wyświetlają się jakiekolwiek informacje o użytkowniku. Przynajmniej nie bez oczekiwania na przekroczenie limitu czasu.

Przykład: fetch()

To działanie polega na zastąpieniu rzeczywistej funkcji lub klasy elementem, który jest pod Twoją kontrolą w teście. Jest to częste w prawie wszystkich typach testów oprócz najprostszych testów jednostkowych. Zostało to omówione dokładniej w sekcji Assercje i inne elementy podstawowe.

Możesz symulować funkcję fetch() do testu, aby szybko się zakończył i zwracał oczekiwane dane, a nie dane „rzeczywiste” lub nieznane. fetch ma charakter globalny, co oznacza, że nie musimy go umieszczać w kodzie import ani require.

W vitest możesz wymazać globalny, wywołując vi.stubGlobal ze specjalnym obiektem zwróconym przez vi.fn() – dzięki temu można utworzyć makietę, którą będziemy mogli później zmodyfikować. Metody te omówimy bardziej szczegółowo w dalszej części szkolenia, ale ich praktyczne zastosowanie znajdziesz w tym kodzie:

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

Ten kod dodaje próbkę, opisuje „fałszywą” wersję sieci pobierania (Response), a następnie czeka, aż się pojawi. Jeśli tekst się nie pojawi – możesz to sprawdzić, zmieniając zapytanie w komponencie queryByText na nową nazwę – test się nie powiedzie.

W tym przykładzie wykorzystywano wbudowane narzędzia do żartowania Vitesta, ale inne metody testowe stosują podobne podejście do żartów. Vitest jest wyjątkowy pod tym względem, że po wszystkich testach musisz wywołać vi.unstubAllGlobals() lub ustawić równoważną opcję globalną. Bez „cofnięcia” naszej pracy makieta fetch może wpłynąć na inne testy, a na każde żądanie jest wysyłane nasz dziwny stos JSON.

Przykłady importu

Jak można zauważyć, sam komponent UserList importuje komponent o nazwie UserRow. Nie umieściliśmy w nim kodu, ale możesz zobaczyć, że renderuje on nazwę użytkownika. Poprzednie testy dotyczą imienia i nazwiska „Sam”, które nie są renderowane bezpośrednio w elemencie UserList, dlatego musi on pochodzić z interfejsu UserRow.

Schemat blokowy przedstawiający, jak nazwy użytkowników poruszają się w obrębie naszego komponentu.
UserListTest nie ma widoczności UserRow.

Jednak UserRow może być złożonym komponentem – może pobierać dalsze dane użytkownika lub mieć skutki uboczne nieistotne dla naszego testu. Usunięcie tej zmienności sprawi, że testy staną się bardziej pomocne, zwłaszcza że komponenty, które chcesz przetestować, staną się bardziej złożone i częściej się wiążą z zależnościami.

Na szczęście możesz imitować niektóre importowanie za pomocą Vitest, nawet jeśli w Twoim teście nie są one stosowane bezpośrednio, dzięki czemu każdy korzystający z nich kod był udostępniany w prostej lub znanej wersji:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

Jest to potężne narzędzie, podobnie jak naśmiewanie się z tabeli fetch. Może jednak okazać się niewykonalne, jeśli Twój kod jest uzależniony od wielu czynników. Najlepszym rozwiązaniem jest napisanie testowalnego kodu.

Kliknij i podaj kontekst

React i inne biblioteki takie jak Lit mają pojęcie Context. Przykładowy kod zawiera element UserContext, który wywołuje metodę w przypadku wyboru użytkownika. Często jest to alternatywa dla „analizowania zasobów”, w ramach której wywołanie zwrotne jest przekazywane bezpośrednio do UserList.

Opracowana przez nas jarzma testowa nie zapewniła dostępu do UserContext. Dodanie do testu reakcji bez tego działania kliknięcia spowoduje w najgorszym razie jej zakłócenie, a w najgorszym razie – jeśli instancja domyślna została udostępniona w innym miejscu – spowoduje to pewne zachowanie, którego nie możemy kontrolować (podobne do nieznanych elementów UserRow powyżej):

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

Podczas renderowania komponentu możesz zamiast tego udostępnić własny Context. W tym przykładzie korzystamy z przykładowej vi.fn() funkcji Vitest Mock, której można użyć już po fakcie do sprawdzenia, czy wywołano wywołanie i jakie argumenty zostały użyte. W naszym przypadku oddziałuje to na fałszywą wartość fetch z poprzedniego przykładu i test może potwierdzić, że przekazany identyfikator to „sam”:

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

To prosty, ale skuteczny wzorzec, który umożliwia usuwanie nieistotnych zależności od podstawowego komponentu, który próbujesz przetestować.

W skrócie

To był krótki i uproszczony przykład pokazujący, jak utworzyć test komponentów, aby przetestować i zabezpieczyć trudny do przetestowania komponent React. Skupiamy się na tym, by prawidłowo współdziałać z jego zależnościami (globalnym fetch, zaimportowanym podkomponentem i Context).

Sprawdź swoją wiedzę

W jaki sposób testowano komponent React?

Imitowanie złożonych zależności za pomocą prostych zależności do przetestowania.
Wstrzykiwanie zależności na podstawie kontekstu
Łączenie globalne
Sprawdzanie, czy liczba zwiększyła się