Kategoria:React

React Testing Library - podstawy

  • Czas potrzebny na przeczytanie:5 minut
  • Opublikowane:22 marca 2021

React Testing Library to najpopularniejsza biblioteka do testowania kodu napisanego w React. Wchodzi ona w skład większej całości - DOM Testing Library. Jest ona tak naprawdę tylko jednym z portów, podobne rozwiązania do niej możemy znaleźć chociażby we Vue i w Angularze.

RTL*, w przeciwieństwie do jej poprzedników, podchodzi do testowania z perspektywy użytkownika. Nie testujemy w niej tzw. implementation details, to znaczy, że np. nie sprawdzamy, czy nasz stan się zmienił. Zamiast tego oczekujemy, że np. nasz użytkownik zobaczył odpowiedni komunikat na ekranie.

*React Testing Library. Na potrzeby tego materiału będę używał tego skrótu na przemian wraz z pełną nazwą biblioteki.

Podstawy

RTL nie jest samodzielną biblioteką, jak już wspominałem, jest ona tylko nakładką na DOM Testing Library, ale poza tym, w naszych testach, będziemy korzystać jeszcze z Jest, w tym przypadku w roli tzw. test runnera.

Jeśli jeszcze nie znasz Jest, to zachęcam Cię do zapoznania się z serią poświęconej tej technologii. Ta wiedza będzie Ci niezbędna, by zacząć przygodę z testowaniem aplikacji Reaktowych

Konfiguracja projektu

Jeśli chcesz pominąć konfigurację projektu, to możesz skorzystać z przygotowanego startera na GitHubie.

Zacznijmy od stworzenia aplikacji za pomocą Create React App:

npx create-react-app my-app --template typescript

Potrzebujemy również zainstalować potrzebne paczki:

npm install --save-dev @testing-library/jest-dom @testing-library/react @testing-library/user-event ts-jest

Eslint

Poza Twoją standardową konfiguracja warto dodać dwa pluginy, które ułatwią nam pracę z testami:

npm install --save-dev eslint-plugin-jest-dom eslint-plugin-testing-library
{
  "plugins": ["jest-dom", "testing-library", ...]
}

Jest

Konfigurację Jest zamieścimy w pliku jest.config.js w katalogu głównym projektu. To, co nas tutaj najbardziej interesuję to podanie ścieżki do pliku setupTests.ts, do którego za chwilę wrócimy.

module.exports = {
  preset: 'ts-jest',
  roots: ['<rootDir>'],
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  moduleFileExtensions: ['js', 'ts', 'tsx', 'json'],
  collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts', '!**/node_modules/**'],
  transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$'],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
  },
  moduleNameMapper: {
    '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
  },
};

Testy w praktyce

W ramach naszego pierwszego testu przetestujemy sobie prosty komponent licznika:

import { useState } from 'react';

export const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount((prevState) => prevState + 1);
  };

  const decrement = () => {
    setCount((prevState) => prevState - 1);
  };

  return (
    <>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <span>count: {count}</span>
    </>
  );
};

Będziemy chcieli tutaj przetestować, czy count zmienia swoją wartości po wciśnięciu odpowiedniego przycisku. Obok naszego komponentu stwórzmy plik Counter.spec.ts, w którym będą znajdowały się testy.

import { render, screen } from '@testing-library/react';
import { Counter } from './Counter';

describe('Counter', () => {
  it('increments the count', () => {
    render(<Counter />);

    screen.debug();
  });
});

Funkcji render() używamy do wyrenderowania komponentu. Po wyrenderowaniu mamy dostęp do specjalnego obiektu screen, który posiada metodę debug(). Dzięki niej będziemy mogli podejrzeć aktualną strukturę komponentu:

Struktura komponentu licznika

Zapytania

Następnym naszym krokiem będzie pobranie przycisku, który jest elementem DOM w komponencie Counter. Do pobierania wspomnianych elementów służą następujące metody obiektu screen:

  • getBy* - zwraca pasujący element lub rzuca wyjątkiem.

  • queryBy* - zwraca pasujący element lub null.

  • findBy* - zwraca Promise, który rozwiązuję się po znalezieniu elementu. Jeśli nie znaleziono elementu, to Promise zostaje odrzucony po 1 sekundzie.

Każda z tych opcji posiada wiele sposobów na pobranie elementu. Mamy np. zapytania getByRole, getByLabelText, getByText, getByTestId itp. Analogicznie, są one dostępne również dla queryBy* i findBy*.

Zapytania typu queryBy przydadzą się, gdy chcemy skorzystać z asercji i sprawdzić, czy np. danego elementu nie ma w drzewie DOM:

const button = screen.queryByRole('button');
expect(button).not.toBeInTheDocument();

findBy* za to przydadzą się przy pracy z asynchronicznym kodem, do którego jeszcze wrócimy.

W naszym przypadku będziemy korzystać ze zwykłych zapytań getBy*. Dobrą praktyką jest używanie w większości przypadków getByRole. Dzięki getByRole możemy pobierać elementy, które znajdują się w drzewku dostępności. Drugim argumentem zapytania jest obiekt z opcjami, w którym możemy podać tzw. accessibility name.

Powinniśmy unikać za to zapytań z pomocą getByTestId, nie wspisują się one w myśl zasady RTL o testowaniu przez pryzmat użytkownika.

Wróćmy do naszego testu, za pomocą zapytań typu getBy* pobieramy dwa elementy DOM, podajemy w nich nazwę w formie wyrażenia regularnego. Następnie sprawdzamy, czy count posiada odpowiedni tekst.

import { render, screen } from '@testing-library/react';
import { Counter } from './Counter';
import '@testing-library/jest-dom/extend-expect';

describe('Counter', () => {
  it('increments the count', () => {
    render(<Counter />);

    screen.debug();

    const button = screen.getByRole('button', { name: /increment/ });

    const count = screen.getByText(/count/);

    expect(count).toHaveTextContent('count: 0');
  });
});

Zwroć uwagę na ostatni import. To dzięki niemu mamy dostęp do macherów typu toHaveTextContent, toHaveFocus, toBeInTheDocument itp. Pamiętasz jak mówiliśmy o pliku setupTests.ts? Jeśli zaimportujesz do niego @testing-library/jest-dom, to nie będziesz musiał/a robić tego w każdym pliku z testami!

Zdarzenia

W React Testing Library mamy do dyspozycji dwie opcje symulowania eventów - fireEvent i userEvent. My będziemy korzystać z tej drugiej metody, jest ona rekomendowana przez twórców oraz oddaje bardziej zachowania prawdziwego użytkownika.

W naszym przypadku musimy skorzystać z metody click(), do której przekazujemy wcześniej pobrany przycisk:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counter', () => {
  it('increments the count', () => {
    render(<Counter />);

    screen.debug();

    const button = screen.getByRole('button', { name: /increment/ });

    const count = screen.getByText(/count/);

    expect(count).toHaveTextContent('count: 0');

    userEvent.click(button);

    expect(count).toHaveTextContent('count: 1');
  });
});

Po wykonaniu zdarzenia ponownie sprawdzamy poprawność tekstu.

Pozostało nam jeszcze sprawdzić działanie drugiego scenariusza, wtedy gdy będziemy zmniejszać licznik, ale to już pozostawiam Ci w ramach ćwiczeń :)

Podsumowanie

Cały kod z dzisiejszego artykułu możesz znaleźć w repozytorium na GitHubie.

Jeśli chcesz przećwiczyć testy w praktyce, to zachęcam Cię do sprawdzenia Testing Playground.

Do usłyszenia!

Źródła

O autorze

Olaf Sulich

Olaf jest Frontend Developerem, blogerem i nosi rybacki kapelusz 🎩 Pisze o wszystkim co związane z frontendem, ale nie boi się backendu i designów 🦾 Ma głowę pełną pomysłów i nadzieję, że znajdziesz tutaj coś dla siebie!