Polski

Opanuj React Testing Library (RTL) dzięki temu kompletnemu przewodnikowi. Naucz się pisać skuteczne, łatwe w utrzymaniu i zorientowane na użytkownika testy dla swoich aplikacji React, koncentrując się na najlepszych praktykach i rzeczywistych przykładach.

React Testing Library: Kompleksowy Przewodnik

W dzisiejszym dynamicznym świecie tworzenia aplikacji internetowych zapewnienie jakości i niezawodności aplikacji React jest najważniejsze. React Testing Library (RTL) stała się popularnym i skutecznym rozwiązaniem do pisania testów, które koncentrują się na perspektywie użytkownika. Ten przewodnik zapewnia kompletny przegląd RTL, obejmujący wszystko od podstawowych koncepcji po zaawansowane techniki, co pozwoli Ci budować solidne i łatwe w utrzymaniu aplikacje React.

Dlaczego warto wybrać React Testing Library?

Tradycyjne podejścia do testowania często opierają się na szczegółach implementacji, co sprawia, że testy są kruche i podatne na awarie przy drobnych zmianach w kodzie. Z drugiej strony, RTL zachęca do testowania komponentów w taki sposób, w jaki wchodziłby z nimi w interakcję użytkownik, skupiając się na tym, co użytkownik widzi i czego doświadcza. To podejście oferuje kilka kluczowych zalet:

Konfiguracja środowiska testowego

Zanim zaczniesz używać RTL, musisz skonfigurować swoje środowisko testowe. Zazwyczaj obejmuje to instalację niezbędnych zależności i konfigurację frameworka testowego.

Wymagania wstępne

Instalacja

Zainstaluj następujące pakiety za pomocą npm lub yarn:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Lub, używając yarn:

yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Objaśnienie pakietów:

Konfiguracja

Utwórz plik `babel.config.js` w głównym katalogu projektu o następującej treści:

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

Zaktualizuj swój plik `package.json`, aby dodać skrypt testowy:

{
  "scripts": {
    "test": "jest"
  }
}

Utwórz plik `jest.config.js` w głównym katalogu projektu, aby skonfigurować Jest. Minimalna konfiguracja może wyglądać tak:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['/src/setupTests.js'],
};

Utwórz plik `src/setupTests.js` o następującej treści. Zapewni to, że "matchery" Jest DOM będą dostępne we wszystkich Twoich testach:

import '@testing-library/jest-dom/extend-expect';

Pisanie pierwszego testu

Zacznijmy od prostego przykładu. Załóżmy, że masz komponent React, który wyświetla wiadomość powitalną:

// src/components/Greeting.js
import React from 'react';

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

export default Greeting;

Teraz napiszmy test dla tego komponentu:

// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('renderuje wiadomość powitalną', () => {
  render(<Greeting name="World" />);
  const greetingElement = screen.getByText(/Hello, World!/i);
  expect(greetingElement).toBeInTheDocument();
});

Objaśnienie:

Aby uruchomić test, wykonaj następujące polecenie w terminalu:

npm test

Jeśli wszystko jest poprawnie skonfigurowane, test powinien przejść pomyślnie.

Popularne zapytania (Queries) w RTL

RTL dostarcza różne metody zapytań do znajdowania elementów w DOM. Te zapytania są zaprojektowane tak, aby naśladować sposób, w jaki użytkownicy wchodzą w interakcję z Twoją aplikacją.

`getByRole`

To zapytanie znajduje element po jego roli ARIA. Dobrą praktyką jest używanie `getByRole` zawsze, gdy to możliwe, ponieważ promuje to dostępność i zapewnia, że testy są odporne na zmiany w bazowej strukturze DOM.

<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();

`getByLabelText`

To zapytanie znajduje element po tekście powiązanej z nim etykiety. Jest to przydatne do testowania elementów formularza.

<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();

`getByPlaceholderText`

To zapytanie znajduje element po jego tekście zastępczym (placeholder).

<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();

`getByAltText`

To zapytanie znajduje element obrazu po jego tekście alternatywnym (alt text). Ważne jest, aby zapewnić znaczący tekst alternatywny dla wszystkich obrazów, aby zapewnić dostępność.

<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();

`getByTitle`

To zapytanie znajduje element po jego atrybucie `title`.

<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();

`getByDisplayValue`

To zapytanie znajduje element po jego wyświetlanej wartości. Jest to przydatne do testowania pól formularza z wstępnie wypełnionymi wartościami.

<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();

Zapytania `getAllBy*`

Oprócz zapytań `getBy*`, RTL dostarcza również zapytania `getAllBy*`, które zwracają tablicę pasujących elementów. Są one przydatne, gdy chcesz sprawdzić, czy w DOM znajduje się wiele elementów o tych samych cechach.

<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);

Zapytania `queryBy*`

Zapytania `queryBy*` są podobne do zapytań `getBy*`, ale zwracają `null`, jeśli nie znaleziono pasującego elementu, zamiast rzucać błąd. Jest to przydatne, gdy chcesz sprawdzić, czy element *nie* jest obecny w DOM.

const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();

Zapytania `findBy*`

Zapytania `findBy*` są asynchronicznymi wersjami zapytań `getBy*`. Zwracają one obietnicę (Promise), która zostaje rozwiązana, gdy pasujący element zostanie znaleziony. Są one przydatne do testowania operacji asynchronicznych, takich jak pobieranie danych z API.

// Symulacja asynchronicznego pobierania danych
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('Data Loaded!'), 1000);
});

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
test('ładuje dane asynchronicznie', async () => {
  render(<MyComponent />);
  const dataElement = await screen.findByText('Data Loaded!');
  expect(dataElement).toBeInTheDocument();
});

Symulowanie interakcji użytkownika

RTL dostarcza API `fireEvent` i `userEvent` do symulowania interakcji użytkownika, takich jak klikanie przycisków, wpisywanie tekstu w polach wejściowych i przesyłanie formularzy.

`fireEvent`

`fireEvent` pozwala na programowe wywoływanie zdarzeń DOM. Jest to API niższego poziomu, które daje szczegółową kontrolę nad wywoływanymi zdarzeniami.

<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';

test('symuluje kliknięcie przycisku', () => {
  const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
  render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
  const buttonElement = screen.getByRole('button');
  fireEvent.click(buttonElement);
  expect(alertMock).toHaveBeenCalledTimes(1);
  alertMock.mockRestore();
});

`userEvent`

`userEvent` to API wyższego poziomu, które symuluje interakcje użytkownika w bardziej realistyczny sposób. Obsługuje ono takie szczegóły, jak zarządzanie focusem i kolejność zdarzeń, dzięki czemu testy są bardziej solidne i mniej kruche.

<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';

test('symuluje wpisywanie tekstu w polu wejściowym', () => {
  const inputElement = screen.getByRole('textbox');
  userEvent.type(inputElement, 'Hello, world!');
  expect(inputElement).toHaveValue('Hello, world!');
});

Testowanie kodu asynchronicznego

Wiele aplikacji React obejmuje operacje asynchroniczne, takie jak pobieranie danych z API. RTL dostarcza kilka narzędzi do testowania kodu asynchronicznego.

`waitFor`

`waitFor` pozwala poczekać, aż warunek stanie się prawdziwy, przed dokonaniem asercji. Jest to przydatne do testowania operacji asynchronicznych, których ukończenie zajmuje trochę czasu.

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    setTimeout(() => {
      setData('Data loaded!');
    }, 1000);
  }, []);

  return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';

test('czeka na załadowanie danych', async () => {
  render(<MyComponent />);
  await waitFor(() => screen.getByText('Data loaded!'));
  const dataElement = screen.getByText('Data loaded!');
  expect(dataElement).toBeInTheDocument();
});

Zapytania `findBy*`

Jak wspomniano wcześniej, zapytania `findBy*` są asynchroniczne i zwracają obietnicę (Promise), która zostaje rozwiązana, gdy pasujący element zostanie znaleziony. Są one przydatne do testowania operacji asynchronicznych, które skutkują zmianami w DOM.

Testowanie hooków

Hooki React to funkcje wielokrotnego użytku, które hermetyzują logikę stanową. RTL dostarcza narzędzie `renderHook` z `@testing-library/react-hooks` (które jest przestarzałe na rzecz `@testing-library/react` od wersji 17) do testowania niestandardowych hooków w izolacji.

// src/hooks/useCounter.js
import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

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

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

  return { count, increment, decrement };
}

export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('inkrementuje licznik', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Objaśnienie:

Zaawansowane techniki testowania

Gdy już opanujesz podstawy RTL, możesz zgłębić bardziej zaawansowane techniki testowania, aby poprawić jakość i łatwość utrzymania swoich testów.

Mockowanie modułów

Czasami może być konieczne mockowanie zewnętrznych modułów lub zależności, aby wyizolować komponenty i kontrolować ich zachowanie podczas testowania. Jest dostarcza do tego celu potężne API do mockowania.

// src/api/dataService.js
export const fetchData = async () => {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';

jest.mock('../api/dataService');

test('pobiera dane z API', async () => {
  dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });

  render(<MyComponent />);

  await waitFor(() => screen.getByText('Mocked data!'));

  expect(screen.getByText('Mocked data!')).toBeInTheDocument();
  expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});

Objaśnienie:

Dostawcy kontekstu (Context Providers)

Jeśli Twój komponent polega na dostawcy kontekstu (Context Provider), będziesz musiał opakować go w ten dostawca podczas testowania. Zapewni to komponentowi dostęp do wartości kontekstu.

// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';

test('przełącza motyw', () => {
  render(
    <ThemeProvider>
      <MyComponent />
    </ThemeProvider>
  );

  const themeParagraph = screen.getByText(/Current theme: light/i);
  const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });

  expect(themeParagraph).toBeInTheDocument();

  fireEvent.click(toggleButton);

  expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});

Objaśnienie:

Testowanie z Routerem

Podczas testowania komponentów, które używają React Router, będziesz musiał dostarczyć zmockowany kontekst Routera. Możesz to osiągnąć za pomocą komponentu `MemoryRouter` z `react-router-dom`.

// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';

function MyComponent() {
  return (
    <div>
      <Link to="/about">Go to About Page</Link>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';

test('renderuje link do strony "O nas"', () => {
  render(
    <MemoryRouter>
      <MyComponent />
    </MemoryRouter>
  );

  const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
  expect(linkElement).toBeInTheDocument();
  expect(linkElement).toHaveAttribute('href', '/about');
});

Objaśnienie:

Najlepsze praktyki pisania skutecznych testów

Oto kilka najlepszych praktyk, których należy przestrzegać podczas pisania testów z RTL:

Podsumowanie

React Testing Library to potężne narzędzie do pisania skutecznych, łatwych w utrzymaniu i zorientowanych na użytkownika testów dla Twoich aplikacji React. Stosując zasady i techniki opisane w tym przewodniku, możesz tworzyć solidne i niezawodne aplikacje, które spełniają potrzeby użytkowników. Pamiętaj, aby skupić się na testowaniu z perspektywy użytkownika, unikać testowania szczegółów implementacji oraz pisać jasne i zwięzłe testy. Przyjmując RTL i stosując najlepsze praktyki, możesz znacznie poprawić jakość i łatwość utrzymania swoich projektów React, niezależnie od Twojej lokalizacji czy specyficznych wymagań globalnej publiczności.