Savladajte React Testing Library (RTL) uz ovaj potpuni vodič. Naučite pisati učinkovite, održive testove usmjerene na korisnika za vaše React aplikacije, s fokusom na najbolje prakse i primjere iz stvarnog svijeta.
React Testing Library: Sveobuhvatan vodič
U današnjem brzom svijetu web razvoja, osiguravanje kvalitete i pouzdanosti vaših React aplikacija je od presudne važnosti. React Testing Library (RTL) se istaknuo kao popularno i učinkovito rješenje za pisanje testova koji se fokusiraju na korisničku perspektivu. Ovaj vodič pruža cjelovit pregled RTL-a, pokrivajući sve od temeljnih koncepata do naprednih tehnika, osnažujući vas za izradu robusnih i održivih React aplikacija.
Zašto odabrati React Testing Library?
Tradicionalni pristupi testiranju često se oslanjaju na detalje implementacije, čineći testove krhkima i podložnima pucanju pri manjim promjenama koda. RTL, s druge strane, potiče vas da testirate svoje komponente onako kako bi korisnik s njima komunicirao, fokusirajući se na ono što korisnik vidi i doživljava. Ovaj pristup nudi nekoliko ključnih prednosti:
- Testiranje usmjereno na korisnika: RTL promiče pisanje testova koji odražavaju korisničku perspektivu, osiguravajući da vaša aplikacija funkcionira kako se očekuje s gledišta krajnjeg korisnika.
- Smanjena krhkost testova: Izbjegavanjem testiranja detalja implementacije, RTL testovi imaju manju vjerojatnost da će se pokvariti kada refaktorirate svoj kod, što dovodi do održivijih i robusnijih testova.
- Poboljšan dizajn koda: RTL vas potiče na pisanje komponenti koje su pristupačne i jednostavne za korištenje, što dovodi do boljeg cjelokupnog dizajna koda.
- Fokus na pristupačnost: RTL olakšava testiranje pristupačnosti vaših komponenti, osiguravajući da je vaša aplikacija upotrebljiva za sve.
- Pojednostavljen proces testiranja: RTL pruža jednostavan i intuitivan API, olakšavajući pisanje i održavanje testova.
Postavljanje okruženja za testiranje
Prije nego što počnete koristiti RTL, morate postaviti svoje okruženje za testiranje. To obično uključuje instaliranje potrebnih ovisnosti i konfiguriranje vašeg okvira za testiranje.
Preduvjeti
- Node.js i npm (ili yarn): Provjerite imate li Node.js i npm (ili yarn) instalirane na vašem sustavu. Možete ih preuzeti sa službene web stranice Node.js-a.
- React projekt: Trebali biste imati postojeći React projekt ili stvoriti novi pomoću alata Create React App ili sličnog alata.
Instalacija
Instalirajte sljedeće pakete koristeći npm ili yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Ili, koristeći yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Objašnjenje paketa:
- @testing-library/react: Osnovna biblioteka za testiranje React komponenti.
- @testing-library/jest-dom: Pruža prilagođene Jest matchere za provjeru DOM čvorova.
- Jest: Popularni JavaScript okvir za testiranje.
- babel-jest: Jest transformer koji koristi Babel za kompajliranje vašeg koda.
- @babel/preset-env: Babel preset koji određuje Babel plugine i presete potrebne za podršku vašim ciljanim okruženjima.
- @babel/preset-react: Babel preset za React.
Konfiguracija
Kreirajte datoteku `babel.config.js` u korijenu vašeg projekta sa sljedećim sadržajem:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Ažurirajte svoju `package.json` datoteku kako biste uključili skriptu za testiranje:
{
"scripts": {
"test": "jest"
}
}
Kreirajte datoteku `jest.config.js` u korijenu vašeg projekta kako biste konfigurirali Jest. Minimalna konfiguracija mogla bi izgledati ovako:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
};
Kreirajte datoteku `src/setupTests.js` sa sljedećim sadržajem. Ovo osigurava da su Jest DOM matcheri dostupni u svim vašim testovima:
import '@testing-library/jest-dom/extend-expect';
Pisanje vašeg prvog testa
Počnimo s jednostavnim primjerom. Pretpostavimo da imate React komponentu koja prikazuje pozdravnu poruku:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Pozdrav, {name}!</h1>;
}
export default Greeting;
Sada, napišimo test za ovu komponentu:
// 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="Svijete" />);
const greetingElement = screen.getByText(/Pozdrav, Svijete!/i);
expect(greetingElement).toBeInTheDocument();
});
Objašnjenje:
- `render`: Ova funkcija renderira komponentu u DOM.
- `screen`: Ovaj objekt pruža metode za postavljanje upita DOM-u.
- `getByText`: Ova metoda pronalazi element po njegovom tekstualnom sadržaju. Zastavica `/i` čini pretragu neosjetljivom na velika i mala slova.
- `expect`: Ova funkcija se koristi za postavljanje tvrdnji o ponašanju komponente.
- `toBeInTheDocument`: Ovaj matcher potvrđuje da je element prisutan u DOM-u.
Da biste pokrenuli test, izvršite sljedeću naredbu u vašem terminalu:
npm test
Ako je sve ispravno konfigurirano, test bi trebao proći.
Uobičajeni RTL upiti
RTL pruža različite metode upita za pronalaženje elemenata u DOM-u. Ovi upiti su dizajnirani da oponašaju kako korisnici stupaju u interakciju s vašom aplikacijom.
`getByRole`
Ovaj upit pronalazi element po njegovoj ARIA ulozi. Dobra je praksa koristiti `getByRole` kad god je to moguće, jer promiče pristupačnost i osigurava da su vaši testovi otporni na promjene u osnovnoj DOM strukturi.
<button role="button">Klikni me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Ovaj upit pronalazi element po tekstu njegove povezane oznake (label). Koristan je za testiranje elemenata obrasca.
<label htmlFor="name">Ime:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Ime:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Ovaj upit pronalazi element po njegovom placeholder tekstu.
<input type="text" placeholder="Unesite svoj email" />
const emailInputElement = screen.getByPlaceholderText('Unesite svoj email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Ovaj upit pronalazi element slike po njegovom alt tekstu. Važno je osigurati smislen alt tekst za sve slike kako bi se osigurala pristupačnost.
<img src="logo.png" alt="Logo tvrtke" />
const logoImageElement = screen.getByAltText('Logo tvrtke');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Ovaj upit pronalazi element po njegovom title atributu.
<span title="Zatvori">X</span>
const closeElement = screen.getByTitle('Zatvori');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Ovaj upit pronalazi element po njegovoj prikazanoj vrijednosti. Ovo je korisno za testiranje unosa u obrascima s unaprijed popunjenim vrijednostima.
<input type="text" value="Početna vrijednost" />
const inputElement = screen.getByDisplayValue('Početna vrijednost');
expect(inputElement).toBeInTheDocument();
`getAllBy*` upiti
Osim `getBy*` upita, RTL također pruža `getAllBy*` upite, koji vraćaju niz podudarajućih elemenata. Oni su korisni kada trebate potvrditi da je više elemenata s istim karakteristikama prisutno u DOM-u.
<li>Stavka 1</li>
<li>Stavka 2</li>
<li>Stavka 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
`queryBy*` upiti
`queryBy*` upiti su slični `getBy*` upitima, ali vraćaju `null` ako nije pronađen odgovarajući element, umjesto da bace grešku. Ovo je korisno kada želite potvrditi da element *nije* prisutan u DOM-u.
const missingElement = screen.queryByText('Nepostojeći tekst');
expect(missingElement).toBeNull();
`findBy*` upiti
`findBy*` upiti su asinkrone verzije `getBy*` upita. Vraćaju Promise koji se rješava kada se pronađe odgovarajući element. Korisni su za testiranje asinkronih operacija, kao što je dohvaćanje podataka s API-ja.
// Simulacija asinkronog dohvaćanja podataka
const fetchData = () => new Promise(resolve => {
setTimeout(() => resolve('Podaci učitani!'), 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('Podaci učitani!');
expect(dataElement).toBeInTheDocument();
});
Simuliranje korisničkih interakcija
RTL pruža `fireEvent` i `userEvent` API-je za simuliranje korisničkih interakcija, kao što su klikanje gumba, upisivanje u polja za unos i slanje obrazaca.
`fireEvent`
`fireEvent` vam omogućuje programsko pokretanje DOM događaja. To je API niže razine koji vam daje finu kontrolu nad događajima koji se pokreću.
<button onClick={() => alert('Gumb je kliknut!')}>Klikni me</button>
import { fireEvent } from '@testing-library/react';
test('simulates a button click', () => {
const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
render(<button onClick={() => alert('Gumb je kliknut!')}>Klikni me</button>);
const buttonElement = screen.getByRole('button');
fireEvent.click(buttonElement);
expect(alertMock).toHaveBeenCalledTimes(1);
alertMock.mockRestore();
});
`userEvent`
`userEvent` je API više razine koji realističnije simulira korisničke interakcije. On se brine o detaljima kao što su upravljanje fokusom i redoslijed događaja, čineći vaše testove robusnijima i manje krhkima.
<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, 'Pozdrav, svijete!');
expect(inputElement).toHaveValue('Pozdrav, svijete!');
});
Testiranje asinkronog koda
Mnoge React aplikacije uključuju asinkrone operacije, kao što je dohvaćanje podataka s API-ja. RTL pruža nekoliko alata za testiranje asinkronog koda.
`waitFor`
`waitFor` vam omogućuje da pričekate da uvjet postane istinit prije nego što napravite tvrdnju. Koristan je za testiranje asinkronih operacija kojima je potrebno neko vrijeme da se dovrše.
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
setTimeout(() => {
setData('Podaci učitani!');
}, 1000);
}, []);
return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';
test('waits for data to load', async () => {
render(<MyComponent />);
await waitFor(() => screen.getByText('Podaci učitani!'));
const dataElement = screen.getByText('Podaci učitani!');
expect(dataElement).toBeInTheDocument();
});
`findBy*` upiti
Kao što je ranije spomenuto, `findBy*` upiti su asinkroni i vraćaju Promise koji se rješava kada se pronađe odgovarajući element. Korisni su za testiranje asinkronih operacija koje rezultiraju promjenama u DOM-u.
Testiranje Hookova
React Hookovi su višekratno upotrebljive funkcije koje enkapsuliraju logiku sa stanjem. RTL pruža `renderHook` uslužni program iz `@testing-library/react-hooks` (koji je zastario u korist izravnog korištenja iz `@testing-library/react` od v17) za testiranje prilagođenih Hookova u izolaciji.
// 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);
});
Objašnjenje:
- `renderHook`: Ova funkcija renderira Hook i vraća objekt koji sadrži rezultat Hooka.
- `act`: Ova funkcija se koristi za omatanje bilo kojeg koda koji uzrokuje ažuriranje stanja. To osigurava da React može ispravno grupirati i obraditi ažuriranja.
Napredne tehnike testiranja
Nakon što savladate osnove RTL-a, možete istražiti naprednije tehnike testiranja kako biste poboljšali kvalitetu i održivost svojih testova.
Mockiranje modula
Ponekad ćete možda trebati mockirati vanjske module ili ovisnosti kako biste izolirali svoje komponente i kontrolirali njihovo ponašanje tijekom testiranja. Jest pruža moćan API za mockiranje u tu svrhu.
// 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: 'Mockirani podaci!' });
render(<MyComponent />);
await waitFor(() => screen.getByText('Mockirani podaci!'));
expect(screen.getByText('Mockirani podaci!')).toBeInTheDocument();
expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});
Objašnjenje:
- `jest.mock('../api/dataService')`: Ova linija mockira `dataService` modul.
- `dataService.fetchData.mockResolvedValue({ message: 'Mockirani podaci!' })`: Ova linija konfigurira mockiranu `fetchData` funkciju da vrati Promise koji se rješava s navedenim podacima.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Ova linija potvrđuje da je mockirana `fetchData` funkcija pozvana jednom.
Context Provideri
Ako vaša komponenta ovisi o Context Provideru, morat ćete omotati svoju komponentu u provider tijekom testiranja. To osigurava da komponenta ima pristup vrijednostima konteksta.
// 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>Trenutna tema: {theme}</p>
<button onClick={toggleTheme}>Promijeni temu</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(/Trenutna tema: light/i);
const toggleButton = screen.getByRole('button', { name: /Promijeni temu/i });
expect(themeParagraph).toBeInTheDocument();
fireEvent.click(toggleButton);
expect(screen.getByText(/Trenutna tema: dark/i)).toBeInTheDocument();
});
Objašnjenje:
- Mi omotavamo `MyComponent` u `ThemeProvider` kako bismo osigurali potreban kontekst tijekom testiranja.
Testiranje s Routerom
Prilikom testiranja komponenti koje koriste React Router, morat ćete osigurati mock Router kontekst. To možete postići korištenjem `MemoryRouter` komponente iz `react-router-dom`.
// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';
function MyComponent() {
return (
<div>
<Link to="/about">Idi na stranicu O nama</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: /Idi na stranicu O nama/i });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', '/about');
});
Objašnjenje:
- Mi omotavamo `MyComponent` u `MemoryRouter` kako bismo osigurali mock Router kontekst.
- Potvrđujemo da element linka ima ispravan `href` atribut.
Najbolje prakse za pisanje učinkovitih testova
Evo nekoliko najboljih praksi koje treba slijediti prilikom pisanja testova s RTL-om:
- Fokusirajte se na korisničke interakcije: Pišite testove koji simuliraju kako korisnici stupaju u interakciju s vašom aplikacijom.
- Izbjegavajte testiranje detalja implementacije: Ne testirajte unutarnje funkcioniranje vaših komponenti. Umjesto toga, usredotočite se na vidljivo ponašanje.
- Pišite jasne i sažete testove: Učinite svoje testove lakima za razumijevanje i održavanje.
- Koristite smislena imena testova: Odaberite imena testova koja točno opisuju ponašanje koje se testira.
- Držite testove izoliranima: Izbjegavajte ovisnosti između testova. Svaki test trebao bi biti neovisan i samostalan.
- Testirajte rubne slučajeve: Ne testirajte samo sretan put. Pobrinite se da testirate i rubne slučajeve i uvjete pogreške.
- Pišite testove prije kodiranja: Razmislite o korištenju Test-Driven Development (TDD) pristupa za pisanje testova prije nego što napišete svoj kod.
- Slijedite "AAA" obrazac: Arrange, Act, Assert (Pripremi, Djeluj, Potvrdi). Ovaj obrazac pomaže u strukturiranju vaših testova i čini ih čitljivijima.
- Neka vaši testovi budu brzi: Spori testovi mogu obeshrabriti programere da ih često pokreću. Optimizirajte svoje testove za brzinu mockiranjem mrežnih zahtjeva i minimiziranjem količine DOM manipulacije.
- Koristite opisne poruke o pogreškama: Kada tvrdnje ne uspiju, poruke o pogreškama trebale bi pružiti dovoljno informacija za brzo identificiranje uzroka neuspjeha.
Zaključak
React Testing Library je moćan alat za pisanje učinkovitih, održivih i korisnički usmjerenih testova za vaše React aplikacije. Slijedeći načela i tehnike navedene u ovom vodiču, možete izraditi robusne i pouzdane aplikacije koje zadovoljavaju potrebe vaših korisnika. Ne zaboravite se usredotočiti na testiranje iz korisničke perspektive, izbjegavati testiranje detalja implementacije i pisati jasne i sažete testove. Prihvaćanjem RTL-a i usvajanjem najboljih praksi, možete značajno poboljšati kvalitetu i održivost svojih React projekata, bez obzira na vašu lokaciju ili specifične zahtjeve vaše globalne publike.