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:
- Testowanie zorientowane na użytkownika: RTL promuje pisanie testów, które odzwierciedlają perspektywę użytkownika, zapewniając, że aplikacja działa zgodnie z oczekiwaniami z punktu widzenia użytkownika końcowego.
- Mniejsza kruchość testów: Unikając testowania szczegółów implementacji, testy RTL są mniej podatne na awarie podczas refaktoryzacji kodu, co prowadzi do bardziej stabilnych i łatwiejszych w utrzymaniu testów.
- Lepszy projekt kodu: RTL zachęca do pisania komponentów, które są dostępne i łatwe w użyciu, co prowadzi do lepszego ogólnego projektu kodu.
- Skupienie na dostępności: RTL ułatwia testowanie dostępności komponentów, zapewniając, że aplikacja jest użyteczna dla wszystkich.
- Uproszczony proces testowania: RTL zapewnia proste i intuicyjne API, ułatwiając pisanie i utrzymywanie testów.
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
- Node.js i npm (lub yarn): Upewnij się, że masz zainstalowane Node.js i npm (lub yarn) w swoim systemie. Możesz je pobrać z oficjalnej strony internetowej Node.js.
- Projekt React: Powinieneś mieć istniejący projekt React lub utworzyć nowy za pomocą Create React App lub podobnego narzędzia.
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:
- @testing-library/react: Główna biblioteka do testowania komponentów React.
- @testing-library/jest-dom: Dostarcza niestandardowe "matchery" Jest do asercji na węzłach DOM.
- Jest: Popularny framework do testowania JavaScript.
- babel-jest: Transformer dla Jest, który używa Babel do kompilacji kodu.
- @babel/preset-env: Preset Babel, który określa wtyczki i presety Babel potrzebne do obsługi docelowych środowisk.
- @babel/preset-react: Preset Babel dla React.
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:
- `render`: Ta funkcja renderuje komponent w DOM.
- `screen`: Ten obiekt dostarcza metody do wyszukiwania w DOM.
- `getByText`: Ta metoda znajduje element po jego treści tekstowej. Flaga `/i` sprawia, że wyszukiwanie jest niewrażliwe na wielkość liter.
- `expect`: Ta funkcja służy do tworzenia asercji dotyczących zachowania komponentu.
- `toBeInTheDocument`: Ten "matcher" sprawdza, czy element jest obecny w DOM.
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:
- `renderHook`: Ta funkcja renderuje hooka i zwraca obiekt zawierający jego wynik.
- `act`: Ta funkcja służy do opakowywania każdego kodu, który powoduje aktualizacje stanu. Zapewnia to, że React może poprawnie przetwarzać i grupować aktualizacje.
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:
- `jest.mock('../api/dataService')`: Ta linia mockuje moduł `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Ta linia konfiguruje zmockowaną funkcję `fetchData` tak, aby zwracała obietnicę (Promise), która rozwiązuje się z podanymi danymi.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Ta linia sprawdza, czy zmockowana funkcja `fetchData` została wywołana jeden raz.
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:
- Opakowujemy `MyComponent` w `ThemeProvider`, aby zapewnić niezbędny kontekst podczas testowania.
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:
- Opakowujemy `MyComponent` w `MemoryRouter`, aby zapewnić zmockowany kontekst Routera.
- Sprawdzamy, czy element linku ma poprawny atrybut `href`.
Najlepsze praktyki pisania skutecznych testów
Oto kilka najlepszych praktyk, których należy przestrzegać podczas pisania testów z RTL:
- Skupiaj się na interakcjach użytkownika: Pisz testy, które symulują, jak użytkownicy wchodzą w interakcję z Twoją aplikacją.
- Unikaj testowania szczegółów implementacji: Nie testuj wewnętrznego działania komponentów. Zamiast tego skup się na obserwowalnym zachowaniu.
- Pisz jasne i zwięzłe testy: Spraw, aby Twoje testy były łatwe do zrozumienia i utrzymania.
- Używaj znaczących nazw testów: Wybieraj nazwy testów, które dokładnie opisują testowane zachowanie.
- Zachowaj izolację testów: Unikaj zależności między testami. Każdy test powinien być niezależny i samowystarczalny.
- Testuj przypadki brzegowe: Nie testuj tylko "szczęśliwej ścieżki". Upewnij się, że testujesz również przypadki brzegowe i warunki błędów.
- Pisz testy przed kodem: Rozważ użycie Test-Driven Development (TDD), aby pisać testy przed napisaniem kodu.
- Stosuj wzorzec "AAA": Arrange, Act, Assert (Przygotuj, Działaj, Sprawdź). Ten wzorzec pomaga w strukturyzowaniu testów i czyni je bardziej czytelnymi.
- Dbaj o szybkość testów: Wolne testy mogą zniechęcać programistów do ich częstego uruchamiania. Optymalizuj swoje testy pod kątem szybkości, mockując żądania sieciowe i minimalizując ilość manipulacji DOM.
- Używaj opisowych komunikatów o błędach: Gdy asercje zawodzą, komunikaty o błędach powinny dostarczać wystarczająco dużo informacji, aby szybko zidentyfikować przyczynę niepowodzenia.
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.