Zvládněte React Testing Library (RTL) s tímto kompletním průvodcem. Naučte se psát efektivní a na uživatele zaměřené testy pro vaše React aplikace.
React Testing Library: Komplexní průvodce
V dnešním rychle se rozvíjejícím světě webového vývoje je zajištění kvality a spolehlivosti vašich React aplikací naprosto klíčové. React Testing Library (RTL) se stala populárním a efektivním řešením pro psaní testů, které se zaměřují na perspektivu uživatele. Tento průvodce poskytuje kompletní přehled RTL, od základních konceptů až po pokročilé techniky, a umožní vám tak vytvářet robustní a udržovatelné React aplikace.
Proč zvolit React Testing Library?
Tradiční přístupy k testování se často spoléhají na detaily implementace, což činí testy křehkými a náchylnými k selhání i při drobných změnách v kódu. RTL naopak podporuje testování komponent tak, jak by s nimi interagoval uživatel, se zaměřením na to, co uživatel vidí a prožívá. Tento přístup nabízí několik klíčových výhod:
- Testování zaměřené na uživatele: RTL podporuje psaní testů, které odrážejí perspektivu uživatele, a zajišťuje tak, že vaše aplikace funguje z pohledu koncového uživatele podle očekávání.
- Snížená křehkost testů: Tím, že se vyhýbá testování implementačních detailů, je méně pravděpodobné, že se testy RTL rozbijí při refaktorování kódu, což vede k udržovatelnějším a robustnějším testům.
- Zlepšený návrh kódu: RTL vás vybízí k psaní komponent, které jsou přístupné a snadno použitelné, což vede k lepšímu celkovému návrhu kódu.
- Důraz na přístupnost: RTL usnadňuje testování přístupnosti vašich komponent a zajišťuje, že vaše aplikace je použitelná pro všechny.
- Zjednodušený proces testování: RTL poskytuje jednoduché a intuitivní API, což usnadňuje psaní a údržbu testů.
Nastavení testovacího prostředí
Než budete moci začít používat RTL, musíte si nastavit testovací prostředí. To obvykle zahrnuje instalaci potřebných závislostí a konfiguraci vašeho testovacího frameworku.
Předpoklady
- Node.js a npm (nebo yarn): Ujistěte se, že máte ve svém systému nainstalovaný Node.js a npm (nebo yarn). Můžete si je stáhnout z oficiálních stránek Node.js.
- React projekt: Měli byste mít existující React projekt nebo si vytvořit nový pomocí Create React App nebo podobného nástroje.
Instalace
Nainstalujte následující balíčky pomocí npm nebo yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Nebo pomocí yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Vysvětlení balíčků:
- @testing-library/react: Základní knihovna pro testování React komponent.
- @testing-library/jest-dom: Poskytuje vlastní Jest matchery pro ověřování DOM uzlů.
- Jest: Populární JavaScriptový testovací framework.
- babel-jest: Jest transformer, který používá Babel ke kompilaci vašeho kódu.
- @babel/preset-env: Babel preset, který určuje, jaké Babel pluginy a presety jsou potřeba pro podporu vašich cílových prostředí.
- @babel/preset-react: Babel preset pro React.
Konfigurace
Vytvořte soubor `babel.config.js` v kořenovém adresáři vašeho projektu s následujícím obsahem:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Aktualizujte svůj soubor `package.json` tak, aby obsahoval testovací skript:
{
"scripts": {
"test": "jest"
}
}
Vytvořte soubor `jest.config.js` v kořenovém adresáři vašeho projektu pro konfiguraci Jestu. Minimální konfigurace může vypadat takto:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
};
Vytvořte soubor `src/setupTests.js` s následujícím obsahem. Tím zajistíte, že Jest DOM matchery budou dostupné ve všech vašich testech:
import '@testing-library/jest-dom/extend-expect';
Psaní prvního testu
Začněme jednoduchým příkladem. Předpokládejme, že máte React komponentu, která zobrazuje uvítací zprávu:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
Nyní pro tuto komponentu napíšeme test:
// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('renders a greeting message', () => {
render(<Greeting name="World" />);
const greetingElement = screen.getByText(/Hello, World!/i);
expect(greetingElement).toBeInTheDocument();
});
Vysvětlení:
- `render`: Tato funkce renderuje komponentu do DOMu.
- `screen`: Tento objekt poskytuje metody pro dotazování se na DOM.
- `getByText`: Tato metoda najde prvek podle jeho textového obsahu. Příznak `/i` činí vyhledávání nerozlišujícím velikost písmen.
- `expect`: Tato funkce se používá k vytváření ověření (assertions) o chování komponenty.
- `toBeInTheDocument`: Tento matcher ověřuje, že prvek je přítomen v DOMu.
Pro spuštění testu spusťte v terminálu následující příkaz:
npm test
Pokud je vše správně nakonfigurováno, test by měl projít.
Běžné RTL dotazy (Queries)
RTL poskytuje různé metody dotazování (queries) pro nalezení prvků v DOMu. Tyto dotazy jsou navrženy tak, aby napodobovaly způsob, jakým uživatelé interagují s vaší aplikací.
`getByRole`
Tento dotaz najde prvek podle jeho ARIA role. Je osvědčeným postupem používat `getByRole` kdykoli je to možné, protože to podporuje přístupnost a zajišťuje, že vaše testy jsou odolné vůči změnám v podkladové struktuře DOMu.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Tento dotaz najde prvek podle textu jeho přidruženého popisku (label). Je to užitečné pro testování prvků formuláře.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Tento dotaz najde prvek podle jeho zástupného textu (placeholder).
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Tento dotaz najde obrázek podle jeho alternativního textu (alt text). Je důležité poskytovat smysluplný alt text pro všechny obrázky, aby byla zajištěna přístupnost.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Tento dotaz najde prvek podle jeho atributu `title`.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Tento dotaz najde prvek podle jeho zobrazené hodnoty (display value). To je užitečné pro testování vstupních polí formuláře s předvyplněnými hodnotami.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
`getAllBy*` dotazy
Kromě `getBy*` dotazů poskytuje RTL také `getAllBy*` dotazy, které vrací pole shodujících se prvků. Jsou užitečné, když potřebujete ověřit, že v DOMu je přítomno více prvků se stejnými vlastnostmi.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
`queryBy*` dotazy
`queryBy*` dotazy jsou podobné `getBy*` dotazům, ale vrací `null`, pokud není nalezen žádný odpovídající prvek, místo aby vyhodily chybu. To je užitečné, když chcete ověřit, že prvek v DOMu *není* přítomen.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
`findBy*` dotazy
`findBy*` dotazy jsou asynchronní verze `getBy*` dotazů. Vracejí Promise, která se vyřeší (resolve), když je nalezen odpovídající prvek. Jsou užitečné pro testování asynchronních operací, jako je načítání dat z API.
// Simulating an asynchronous data fetch
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('loads data asynchronously', async () => {
render(<MyComponent />);
const dataElement = await screen.findByText('Data Loaded!');
expect(dataElement).toBeInTheDocument();
});
Simulace uživatelských interakcí
RTL poskytuje API `fireEvent` a `userEvent` pro simulaci uživatelských interakcí, jako je klikání na tlačítka, psaní do vstupních polí a odesílání formulářů.
`fireEvent`
`fireEvent` vám umožňuje programově spouštět DOM události. Jedná se o nízkoúrovňové API, které vám dává jemnou kontrolu nad spouštěnými událostmi.
<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';
test('simulates a button click', () => {
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` je API vyšší úrovně, které simuluje uživatelské interakce realističtěji. Stará se o detaily, jako je správa fokusu a pořadí událostí, což činí vaše testy robustnějšími a méně křehkými.
<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';
test('simulates typing in an input field', () => {
const inputElement = screen.getByRole('textbox');
userEvent.type(inputElement, 'Hello, world!');
expect(inputElement).toHaveValue('Hello, world!');
});
Testování asynchronního kódu
Mnoho React aplikací zahrnuje asynchronní operace, jako je načítání dat z API. RTL poskytuje několik nástrojů pro testování asynchronního kódu.
`waitFor`
`waitFor` vám umožňuje počkat, až se podmínka stane pravdivou, než provedete ověření. Je to užitečné pro testování asynchronních operací, jejichž dokončení nějakou dobu trvá.
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('waits for data to load', async () => {
render(<MyComponent />);
await waitFor(() => screen.getByText('Data loaded!'));
const dataElement = screen.getByText('Data loaded!');
expect(dataElement).toBeInTheDocument();
});
`findBy*` dotazy
Jak již bylo zmíněno, `findBy*` dotazy jsou asynchronní a vrací Promise, která se vyřeší, když je nalezen odpovídající prvek. Jsou užitečné pro testování asynchronních operací, které vedou ke změnám v DOMu.
Testování Hooků
React Hooky jsou znovupoužitelné funkce, které zapouzdřují stavovou logiku. RTL poskytuje utilitu `renderHook` z `@testing-library/react-hooks` (která je od verze 17 zastaralá ve prospěch `@testing-library/react` přímo) pro testování vlastních Hooků v izolaci.
// 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('increments the counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Vysvětlení:
- `renderHook`: Tato funkce renderuje Hook a vrací objekt obsahující výsledek Hooku.
- `act`: Tato funkce se používá k obalení jakéhokoli kódu, který způsobuje aktualizace stavu. Tím se zajistí, že React může správně dávkovat a zpracovávat aktualizace.
Pokročilé techniky testování
Jakmile zvládnete základy RTL, můžete prozkoumat pokročilejší techniky testování, abyste zlepšili kvalitu a udržovatelnost svých testů.
Mockování modulů
Někdy může být potřeba mockovat externí moduly nebo závislosti, abyste izolovali své komponenty a kontrolovali jejich chování během testování. Jest pro tento účel poskytuje výkonné API pro mockování.
// 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('fetches data from the 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);
});
Vysvětlení:
- `jest.mock('../api/dataService')`: Tento řádek mockuje modul `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Tento řádek konfiguruje mockovanou funkci `fetchData` tak, aby vracela Promise, která se vyřeší se zadanými daty.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Tento řádek ověřuje, že mockovaná funkce `fetchData` byla zavolána jednou.
Context Providers
Pokud vaše komponenta spoléhá na Context Provider, budete ji muset během testování obalit do tohoto provideru. Tím zajistíte, že komponenta bude mít přístup k hodnotám kontextu.
// 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('toggles the theme', () => {
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();
});
Vysvětlení:
- Obalujeme `MyComponent` do `ThemeProvider`, abychom během testování poskytli potřebný kontext.
Testování s Routerem
Při testování komponent, které používají React Router, budete muset poskytnout mockovaný kontext Routeru. Toho můžete dosáhnout pomocí komponenty `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('renders a link to the about page', () => {
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', '/about');
});
Vysvětlení:
- Obalujeme `MyComponent` do `MemoryRouter`, abychom poskytli mockovaný kontext Routeru.
- Ověřujeme, že prvek odkazu má správný atribut `href`.
Osvědčené postupy pro psaní efektivních testů
Zde jsou některé osvědčené postupy, které je dobré dodržovat při psaní testů s RTL:
- Zaměřte se na uživatelské interakce: Pište testy, které simulují, jak uživatelé interagují s vaší aplikací.
- Vyhněte se testování implementačních detailů: Netestujte vnitřní fungování vašich komponent. Místo toho se zaměřte na pozorovatelné chování.
- Pište jasné a stručné testy: Udržujte své testy srozumitelné a snadno udržovatelné.
- Používejte smysluplné názvy testů: Vybírejte názvy testů, které přesně popisují testované chování.
- Udržujte testy izolované: Vyhněte se závislostem mezi testy. Každý test by měl být nezávislý a soběstačný.
- Testujte okrajové případy: Netestujte jen "šťastnou cestu". Ujistěte se, že testujete také okrajové případy a chybové stavy.
- Pište testy před psaním kódu: Zvažte použití Test-Driven Development (TDD) pro psaní testů předtím, než napíšete samotný kód.
- Dodržujte vzor "AAA": Arrange, Act, Assert (Připravit, Provést, Ověřit). Tento vzor pomáhá strukturovat vaše testy a činí je čitelnějšími.
- Udržujte své testy rychlé: Pomalé testy mohou odradit vývojáře od jejich častého spouštění. Optimalizujte rychlost svých testů mockováním síťových požadavků a minimalizací manipulace s DOMem.
- Používejte popisné chybové zprávy: Když selže ověření, chybové zprávy by měly poskytnout dostatek informací k rychlému nalezení příčiny selhání.
Závěr
React Testing Library je mocný nástroj pro psaní efektivních, udržovatelných a na uživatele zaměřených testů pro vaše React aplikace. Dodržováním principů a technik uvedených v tomto průvodci můžete vytvářet robustní a spolehlivé aplikace, které splňují potřeby vašich uživatelů. Nezapomeňte se zaměřit na testování z pohledu uživatele, vyhýbat se testování implementačních detailů a psát jasné a stručné testy. Přijetím RTL a osvědčených postupů můžete výrazně zlepšit kvalitu a udržovatelnost svých React projektů, bez ohledu na vaši lokalitu nebo specifické požadavky vašeho globálního publika.