Italiano

Padroneggia React Testing Library (RTL) con questa guida completa. Impara a scrivere test efficaci, manutenibili e incentrati sull'utente per le tue applicazioni React, con focus su best practice ed esempi reali.

React Testing Library: Guida Completa

Nel panorama odierno dello sviluppo web, in rapida evoluzione, garantire la qualità e l'affidabilità delle tue applicazioni React è fondamentale. React Testing Library (RTL) si è affermata come una soluzione popolare ed efficace per scrivere test che si concentrano sulla prospettiva dell'utente. Questa guida fornisce una panoramica completa di RTL, coprendo tutto, dai concetti fondamentali alle tecniche avanzate, per consentirti di creare applicazioni React robuste e manutenibili.

Perché Scegliere React Testing Library?

Gli approcci di test tradizionali si basano spesso sui dettagli di implementazione, rendendo i test fragili e inclini a rompersi con piccole modifiche al codice. RTL, d'altra parte, ti incoraggia a testare i tuoi componenti come farebbe un utente, concentrandosi su ciò che l'utente vede e sperimenta. Questo approccio offre diversi vantaggi chiave:

Configurazione dell'Ambiente di Test

Prima di poter iniziare a usare RTL, è necessario configurare il proprio ambiente di test. Questo di solito comporta l'installazione delle dipendenze necessarie e la configurazione del framework di test.

Prerequisiti

Installazione

Installa i seguenti pacchetti usando npm o yarn:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Oppure, usando yarn:

yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Spiegazione dei Pacchetti:

Configurazione

Crea un file `babel.config.js` nella root del tuo progetto con il seguente contenuto:

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

Aggiorna il tuo file `package.json` per includere uno script di test:

{
  "scripts": {
    "test": "jest"
  }
}

Crea un file `jest.config.js` nella root del tuo progetto per configurare Jest. Una configurazione minima potrebbe essere questa:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['/src/setupTests.js'],
};

Crea un file `src/setupTests.js` con il seguente contenuto. Ciò garantisce che i matcher DOM di Jest siano disponibili in tutti i tuoi test:

import '@testing-library/jest-dom/extend-expect';

Scrivere il Tuo Primo Test

Iniziamo con un esempio semplice. Supponiamo di avere un componente React che visualizza un messaggio di saluto:

// src/components/Greeting.js
import React from 'react';

function Greeting({ name }) {
  return <h1>Ciao, {name}!</h1>;
}

export default Greeting;

Ora, scriviamo un test per questo componente:

// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('renderizza un messaggio di saluto', () => {
  render(<Greeting name="Mondo" />);
  const greetingElement = screen.getByText(/Ciao, Mondo!/i);
  expect(greetingElement).toBeInTheDocument();
});

Spiegazione:

Per eseguire il test, esegui il seguente comando nel tuo terminale:

npm test

Se tutto è configurato correttamente, il test dovrebbe passare.

Query Comuni di RTL

RTL fornisce vari metodi di query per trovare elementi nel DOM. Queste query sono progettate per imitare il modo in cui gli utenti interagiscono con la tua applicazione.

`getByRole`

Questa query trova un elemento tramite il suo ruolo ARIA. È una buona pratica usare `getByRole` ogni volta che è possibile, poiché promuove l'accessibilità e garantisce che i test siano resilienti ai cambiamenti nella struttura DOM sottostante.

<button role="button">Cliccami</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();

`getByLabelText`

Questa query trova un elemento tramite il testo della sua etichetta associata. È utile per testare gli elementi di un form.

<label htmlFor="name">Nome:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Nome:');
expect(nameInputElement).toBeInTheDocument();

`getByPlaceholderText`

Questa query trova un elemento tramite il suo testo segnaposto.

<input type="text" placeholder="Inserisci la tua email" />
const emailInputElement = screen.getByPlaceholderText('Inserisci la tua email');
expect(emailInputElement).toBeInTheDocument();

`getByAltText`

Questa query trova un elemento immagine tramite il suo testo alternativo (alt text). È importante fornire un testo alternativo significativo per tutte le immagini per garantire l'accessibilità.

<img src="logo.png" alt="Logo Aziendale" />
const logoImageElement = screen.getByAltText('Logo Aziendale');
expect(logoImageElement).toBeInTheDocument();

`getByTitle`

Questa query trova un elemento tramite il suo attributo title.

<span title="Chiudi">X</span>
const closeElement = screen.getByTitle('Chiudi');
expect(closeElement).toBeInTheDocument();

`getByDisplayValue`

Questa query trova un elemento tramite il suo valore visualizzato. È utile per testare input di form con valori precompilati.

<input type="text" value="Valore Iniziale" />
const inputElement = screen.getByDisplayValue('Valore Iniziale');
expect(inputElement).toBeInTheDocument();

Query `getAllBy*`

Oltre alle query `getBy*`, RTL fornisce anche query `getAllBy*`, che restituiscono un array di elementi corrispondenti. Sono utili quando è necessario asserire che più elementi con le stesse caratteristiche sono presenti nel DOM.

<li>Elemento 1</li>
<li>Elemento 2</li>
<li>Elemento 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);

Query `queryBy*`

Le query `queryBy*` sono simili alle query `getBy*`, ma restituiscono `null` se non viene trovato alcun elemento corrispondente, invece di lanciare un errore. Questo è utile quando si vuole asserire che un elemento *non* è presente nel DOM.

const missingElement = screen.queryByText('Testo inesistente');
expect(missingElement).toBeNull();

Query `findBy*`

Le query `findBy*` sono versioni asincrone delle query `getBy*`. Restituiscono una Promise che si risolve quando l'elemento corrispondente viene trovato. Sono utili per testare operazioni asincrone, come il recupero di dati da un'API.

// Simulazione di un recupero dati asincrono
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('Dati Caricati!'), 1000);
});

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
test('carica i dati in modo asincrono', async () => {
  render(<MyComponent />);
  const dataElement = await screen.findByText('Dati Caricati!');
  expect(dataElement).toBeInTheDocument();
});

Simulare le Interazioni dell'Utente

RTL fornisce le API `fireEvent` e `userEvent` per simulare le interazioni dell'utente, come cliccare pulsanti, digitare in campi di input e inviare form.

`fireEvent`

`fireEvent` permette di attivare programmaticamente eventi del DOM. È un'API di livello inferiore che offre un controllo granulare sugli eventi che vengono scatenati.

<button onClick={() => alert('Pulsante cliccato!')}>Cliccami</button>
import { fireEvent } from '@testing-library/react';

test('simula un click del pulsante', () => {
  const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
  render(<button onClick={() => alert('Pulsante cliccato!')}>Cliccami</button>);
  const buttonElement = screen.getByRole('button');
  fireEvent.click(buttonElement);
  expect(alertMock).toHaveBeenCalledTimes(1);
  alertMock.mockRestore();
});

`userEvent`

`userEvent` è un'API di livello superiore che simula le interazioni dell'utente in modo più realistico. Gestisce dettagli come la gestione del focus e l'ordine degli eventi, rendendo i test più robusti e meno fragili.

<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';

test('simula la digitazione in un campo di input', () => {
  const inputElement = screen.getByRole('textbox');
  userEvent.type(inputElement, 'Ciao, mondo!');
  expect(inputElement).toHaveValue('Ciao, mondo!');
});

Testare Codice Asincrono

Molte applicazioni React coinvolgono operazioni asincrone, come il recupero di dati da un'API. RTL fornisce diversi strumenti per testare il codice asincrono.

`waitFor`

`waitFor` consente di attendere che una condizione diventi vera prima di effettuare un'asserzione. È utile per testare operazioni asincrone che richiedono del tempo per essere completate.

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    setTimeout(() => {
      setData('Dati caricati!');
    }, 1000);
  }, []);

  return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';

test('attende il caricamento dei dati', async () => {
  render(<MyComponent />);
  await waitFor(() => screen.getByText('Dati caricati!'));
  const dataElement = screen.getByText('Dati caricati!');
  expect(dataElement).toBeInTheDocument();
});

Query `findBy*`

Come menzionato in precedenza, le query `findBy*` sono asincrone e restituiscono una Promise che si risolve quando viene trovato l'elemento corrispondente. Sono utili per testare operazioni asincrone che comportano modifiche al DOM.

Testare gli Hook

Gli Hook di React sono funzioni riutilizzabili che incapsulano la logica stateful. RTL fornisce l'utilità `renderHook` da `@testing-library/react-hooks` (che è deprecata a favore di `@testing-library/react` direttamente dalla v17) per testare Hook personalizzati in isolamento.

// 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('incrementa il contatore', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Spiegazione:

Tecniche di Test Avanzate

Una volta padroneggiate le basi di RTL, è possibile esplorare tecniche di test più avanzate per migliorare la qualità e la manutenibilità dei test.

Mocking di Moduli

A volte, potrebbe essere necessario effettuare il mock di moduli esterni o dipendenze per isolare i componenti e controllarne il comportamento durante i test. Jest fornisce una potente API di mocking per questo scopo.

// 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 ? data.message : ''}</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('recupera i dati dall\'API', async () => {
  dataService.fetchData.mockResolvedValue({ message: 'Dati mockati!' });

  render(<MyComponent />);

  await waitFor(() => screen.getByText('Dati mockati!'));

  expect(screen.getByText('Dati mockati!')).toBeInTheDocument();
  expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});

Spiegazione:

Context Provider

Se il tuo componente si basa su un Context Provider, dovrai avvolgere il tuo componente nel provider durante il test. Questo assicura che il componente abbia accesso ai valori del contesto.

// 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>Tema attuale: {theme}</p>
      <button onClick={toggleTheme}>Cambia Tema</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('cambia il tema', () => {
  render(
    <ThemeProvider>
      <MyComponent />
    </ThemeProvider>
  );

  const themeParagraph = screen.getByText(/Tema attuale: light/i);
  const toggleButton = screen.getByRole('button', { name: /Cambia Tema/i });

  expect(themeParagraph).toBeInTheDocument();

  fireEvent.click(toggleButton);

  expect(screen.getByText(/Tema attuale: dark/i)).toBeInTheDocument();
});

Spiegazione:

Test con Router

Quando si testano componenti che utilizzano React Router, è necessario fornire un contesto Router fittizio (mock). È possibile ottenere ciò utilizzando il componente `MemoryRouter` da `react-router-dom`.

// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';

function MyComponent() {
  return (
    <div>
      <Link to="/about">Vai alla pagina Chi siamo</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('renderizza un link alla pagina chi siamo', () => {
  render(
    <MemoryRouter>
      <MyComponent />
    </MemoryRouter>
  );

  const linkElement = screen.getByRole('link', { name: /Vai alla pagina Chi siamo/i });
  expect(linkElement).toBeInTheDocument();
  expect(linkElement).toHaveAttribute('href', '/about');
});

Spiegazione:

Best Practice per Scrivere Test Efficaci

Ecco alcune best practice da seguire quando si scrivono test con RTL:

Conclusione

React Testing Library è un potente strumento per scrivere test efficaci, manutenibili e incentrati sull'utente per le tue applicazioni React. Seguendo i principi e le tecniche delineate in questa guida, puoi costruire applicazioni robuste e affidabili che soddisfano le esigenze dei tuoi utenti. Ricorda di concentrarti sul testing dal punto di vista dell'utente, di evitare di testare i dettagli di implementazione e di scrivere test chiari e concisi. Abbracciando RTL e adottando le best practice, puoi migliorare significativamente la qualità e la manutenibilità dei tuoi progetti React, indipendentemente dalla tua posizione o dai requisiti specifici del tuo pubblico globale.