Stăpâniți React Testing Library (RTL) cu acest ghid complet. Învățați cum să scrieți teste eficiente, mentenabile și centrate pe utilizator pentru aplicațiile dvs. React, concentrându-vă pe bune practici și exemple din lumea reală.
React Testing Library: Un Ghid Complet
În peisajul actual al dezvoltării web, aflat într-o continuă accelerare, asigurarea calității și fiabilității aplicațiilor dvs. React este primordială. React Testing Library (RTL) a devenit o soluție populară și eficientă pentru scrierea testelor care se concentrează pe perspectiva utilizatorului. Acest ghid oferă o imagine de ansamblu completă a RTL, acoperind totul, de la conceptele fundamentale la tehnicile avansate, permițându-vă să construiți aplicații React robuste și mentenabile.
De ce să alegeți React Testing Library?
Abordările tradiționale de testare se bazează adesea pe detalii de implementare, ceea ce face testele fragile și predispuse la a eșua la modificări minore ale codului. RTL, pe de altă parte, vă încurajează să testați componentele așa cum ar interacționa un utilizator cu ele, concentrându-vă pe ceea ce vede și experimentează utilizatorul. Această abordare oferă mai multe avantaje cheie:
- Testare centrată pe utilizator: RTL promovează scrierea de teste care reflectă perspectiva utilizatorului, asigurând că aplicația dvs. funcționează așa cum este de așteptat din punctul de vedere al utilizatorului final.
- Reducerea fragilității testelor: Evitând testarea detaliilor de implementare, testele RTL sunt mai puțin predispuse să eșueze atunci când refactorizați codul, ceea ce duce la teste mai mentenabile și mai robuste.
- Design îmbunătățit al codului: RTL vă încurajează să scrieți componente care sunt accesibile și ușor de utilizat, ceea ce duce la un design general mai bun al codului.
- Concentrare pe accesibilitate: RTL facilitează testarea accesibilității componentelor dvs., asigurând că aplicația dvs. poate fi utilizată de oricine.
- Proces de testare simplificat: RTL oferă un API simplu și intuitiv, facilitând scrierea și menținerea testelor.
Configurarea mediului de testare
Înainte de a putea începe să utilizați RTL, trebuie să vă configurați mediul de testare. Acest lucru implică, de obicei, instalarea dependențelor necesare și configurarea framework-ului de testare.
Cerințe preliminare
- Node.js și npm (sau yarn): Asigurați-vă că aveți Node.js și npm (sau yarn) instalate pe sistemul dvs. Le puteți descărca de pe site-ul oficial Node.js.
- Proiect React: Ar trebui să aveți un proiect React existent sau să creați unul nou folosind Create React App sau un instrument similar.
Instalare
Instalați următoarele pachete folosind npm sau yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Sau, folosind yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Explicația pachetelor:
- @testing-library/react: Biblioteca de bază pentru testarea componentelor React.
- @testing-library/jest-dom: Oferă matchers personalizați Jest pentru a face aserțiuni despre nodurile DOM.
- Jest: Un framework popular de testare JavaScript.
- babel-jest: Un transformator Jest care utilizează Babel pentru a compila codul dvs.
- @babel/preset-env: Un preset Babel care determină pluginurile și presetările Babel necesare pentru a susține mediile țintă.
- @babel/preset-react: Un preset Babel pentru React.
Configurare
Creați un fișier `babel.config.js` în rădăcina proiectului dvs. cu următorul conținut:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Actualizați fișierul `package.json` pentru a include un script de test:
{
"scripts": {
"test": "jest"
}
}
Creați un fișier `jest.config.js` în rădăcina proiectului dvs. pentru a configura Jest. O configurație minimală ar putea arăta astfel:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};
Creați un fișier `src/setupTests.js` cu următorul conținut. Acest lucru asigură că matchers Jest DOM sunt disponibili în toate testele dvs.:
import '@testing-library/jest-dom/extend-expect';
Scrierea primului test
Să începem cu un exemplu simplu. Să presupunem că aveți o componentă React care afișează un mesaj de salut:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
Acum, să scriem un test pentru această componentă:
// 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();
});
Explicație:
- `render`: Această funcție randează componenta în DOM.
- `screen`: Acest obiect oferă metode pentru interogarea DOM-ului.
- `getByText`: Această metodă găsește un element după conținutul său text. Steagul `/i` face căutarea insensibilă la majuscule.
- `expect`: Această funcție este utilizată pentru a face aserțiuni despre comportamentul componentei.
- `toBeInTheDocument`: Acest matcher afirmă că elementul este prezent în DOM.
Pentru a rula testul, executați următoarea comandă în terminal:
npm test
Dacă totul este configurat corect, testul ar trebui să treacă.
Interogări (Queries) comune în RTL
RTL oferă diverse metode de interogare pentru găsirea elementelor în DOM. Aceste interogări sunt concepute pentru a imita modul în care utilizatorii interacționează cu aplicația dvs.
`getByRole`
Această interogare găsește un element după rolul său ARIA. Este o bună practică să folosiți `getByRole` ori de câte ori este posibil, deoarece promovează accesibilitatea și asigură că testele dvs. sunt rezistente la modificările structurii DOM subiacente.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Această interogare găsește un element după textul etichetei sale asociate. Este utilă pentru testarea elementelor de formular.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Această interogare găsește un element după textul său substituent (placeholder).
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Această interogare găsește un element imagine după textul său alternativ (alt text). Este important să furnizați text alternativ semnificativ pentru toate imaginile pentru a asigura accesibilitatea.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Această interogare găsește un element după atributul său `title`.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Această interogare găsește un element după valoarea sa afișată. Acest lucru este util pentru testarea câmpurilor de formular cu valori pre-completate.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
Interogări `getAllBy*`
Pe lângă interogările `getBy*`, RTL oferă și interogări `getAllBy*`, care returnează un tablou de elemente corespunzătoare. Acestea sunt utile atunci când trebuie să afirmați că mai multe elemente cu aceleași caracteristici sunt prezente în DOM.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
Interogări `queryBy*`
Interogările `queryBy*` sunt similare cu interogările `getBy*`, dar returnează `null` dacă nu se găsește niciun element corespunzător, în loc să arunce o eroare. Acest lucru este util atunci când doriți să afirmați că un element *nu* este prezent în DOM.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
Interogări `findBy*`
Interogările `findBy*` sunt versiuni asincrone ale interogărilor `getBy*`. Ele returnează o promisiune (Promise) care se rezolvă atunci când elementul corespunzător este găsit. Acestea sunt utile pentru testarea operațiunilor asincrone, cum ar fi preluarea datelor de la un 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();
});
Simularea interacțiunilor utilizatorului
RTL oferă API-urile `fireEvent` și `userEvent` pentru simularea interacțiunilor utilizatorului, cum ar fi apăsarea butoanelor, tastarea în câmpuri de text și trimiterea formularelor.
`fireEvent`
`fireEvent` vă permite să declanșați programatic evenimente DOM. Este un API de nivel inferior care vă oferă un control fin asupra evenimentelor care sunt declanșate.
<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` este un API de nivel superior care simulează interacțiunile utilizatorului mai realist. Acesta gestionează detalii precum gestionarea focusului și ordonarea evenimentelor, făcând testele mai robuste și mai puțin fragile.
<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!');
});
Testarea codului asincron
Multe aplicații React implică operațiuni asincrone, cum ar fi preluarea datelor de la un API. RTL oferă mai multe unelte pentru testarea codului asincron.
`waitFor`
`waitFor` vă permite să așteptați ca o condiție să devină adevărată înainte de a face o aserțiune. Este util pentru testarea operațiunilor asincrone care necesită un anumit timp pentru a se finaliza.
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();
});
Interogări `findBy*`
Așa cum am menționat anterior, interogările `findBy*` sunt asincrone și returnează o promisiune care se rezolvă atunci când elementul corespunzător este găsit. Acestea sunt utile pentru testarea operațiunilor asincrone care duc la modificări în DOM.
Testarea hook-urilor
Hook-urile React sunt funcții reutilizabile care încapsulează logica cu stare. RTL oferă utilitarul `renderHook` de la `@testing-library/react-hooks` (care este depreciat în favoarea `@testing-library/react` direct începând cu v17) pentru testarea hook-urilor personalizate în izolare.
// 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);
});
Explicație:
- `renderHook`: Această funcție randează hook-ul și returnează un obiect care conține rezultatul hook-ului.
- `act`: Această funcție este utilizată pentru a încapsula orice cod care cauzează actualizări de stare. Acest lucru asigură că React poate grupa și procesa corect actualizările.
Tehnici avansate de testare
Odată ce ați stăpânit elementele de bază ale RTL, puteți explora tehnici de testare mai avansate pentru a îmbunătăți calitatea și mentenabilitatea testelor dvs.
Mocking-ul modulelor
Uneori, poate fi necesar să simulați (mock) module sau dependențe externe pentru a izola componentele și a controla comportamentul lor în timpul testării. Jest oferă un API puternic de mocking în acest scop.
// 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);
});
Explicație:
- `jest.mock('../api/dataService')`: Această linie simulează modulul `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Această linie configurează funcția simulată `fetchData` pentru a returna o promisiune care se rezolvă cu datele specificate.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Această linie afirmă că funcția simulată `fetchData` a fost apelată o singură dată.
Provideri de context
Dacă componenta dvs. se bazează pe un Provider de Context, va trebui să încapsulați componenta în provider în timpul testării. Acest lucru asigură că componenta are acces la valorile contextului.
// 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();
});
Explicație:
- Încapsulăm `MyComponent` în `ThemeProvider` pentru a furniza contextul necesar în timpul testării.
Testarea cu Router
Când testați componente care folosesc React Router, va trebui să furnizați un context de Router simulat. Puteți realiza acest lucru folosind componenta `MemoryRouter` din `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');
});
Explicație:
- Încapsulăm `MyComponent` în `MemoryRouter` pentru a furniza un context de Router simulat.
- Afirmăm că elementul link are atributul `href` corect.
Bune practici pentru scrierea testelor eficiente
Iată câteva bune practici de urmat atunci când scrieți teste cu RTL:
- Concentrați-vă pe interacțiunile utilizatorului: Scrieți teste care simulează modul în care utilizatorii interacționează cu aplicația dvs.
- Evitați testarea detaliilor de implementare: Nu testați funcționarea internă a componentelor. În schimb, concentrați-vă pe comportamentul observabil.
- Scrieți teste clare și concise: Faceți testele ușor de înțeles și de întreținut.
- Utilizați nume de teste semnificative: Alegeți nume de teste care descriu cu precizie comportamentul testat.
- Păstrați testele izolate: Evitați dependențele între teste. Fiecare test ar trebui să fie independent și autonom.
- Testați cazurile limită (edge cases): Nu testați doar calea fericită. Asigurați-vă că testați și cazurile limită și condițiile de eroare.
- Scrieți testele înainte de cod: Luați în considerare utilizarea Dezvoltării Ghidate de Teste (TDD) pentru a scrie teste înainte de a scrie codul.
- Urmați modelul "AAA": Arrange (Aranjare), Act (Acțiune), Assert (Aserțiune). Acest model ajută la structurarea testelor și le face mai lizibile.
- Păstrați testele rapide: Testele lente pot descuraja dezvoltatorii să le ruleze frecvent. Optimizați-vă testele pentru viteză prin simularea cererilor de rețea și minimizarea cantității de manipulare a DOM-ului.
- Utilizați mesaje de eroare descriptive: Când aserțiunile eșuează, mesajele de eroare ar trebui să ofere suficiente informații pentru a identifica rapid cauza eșecului.
Concluzie
React Testing Library este un instrument puternic pentru scrierea de teste eficiente, mentenabile și centrate pe utilizator pentru aplicațiile dvs. React. Urmând principiile și tehnicile prezentate în acest ghid, puteți construi aplicații robuste și fiabile care să răspundă nevoilor utilizatorilor dvs. Amintiți-vă să vă concentrați pe testarea din perspectiva utilizatorului, să evitați testarea detaliilor de implementare și să scrieți teste clare și concise. Adoptând RTL și bunele practici, puteți îmbunătăți semnificativ calitatea și mentenabilitatea proiectelor dvs. React, indiferent de locația dvs. sau de cerințele specifice ale publicului dvs. global.