Română

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:

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

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:

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:

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:

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:

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:

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:

Bune practici pentru scrierea testelor eficiente

Iată câteva bune practici de urmat atunci când scrieți teste cu RTL:

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.