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:
- Test Incentrati sull'Utente: RTL promuove la scrittura di test che riflettono la prospettiva dell'utente, garantendo che la tua applicazione funzioni come previsto dal punto di vista dell'utente finale.
- Minore Fragilità dei Test: Evitando di testare i dettagli di implementazione, i test RTL hanno meno probabilità di rompersi quando si effettua il refactoring del codice, portando a test più manutenibili e robusti.
- Miglior Design del Codice: RTL ti incoraggia a scrivere componenti che siano accessibili e facili da usare, portando a un design complessivo del codice migliore.
- Focus sull'Accessibilità: RTL rende più facile testare l'accessibilità dei tuoi componenti, assicurando che la tua applicazione sia utilizzabile da tutti.
- Processo di Testing Semplificato: RTL fornisce un'API semplice e intuitiva, rendendo più facile scrivere e mantenere i test.
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
- Node.js e npm (o yarn): Assicurati di avere Node.js e npm (o yarn) installati sul tuo sistema. Puoi scaricarli dal sito ufficiale di Node.js.
- Progetto React: Dovresti avere un progetto React esistente o crearne uno nuovo usando Create React App o uno strumento simile.
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:
- @testing-library/react: La libreria principale per testare i componenti React.
- @testing-library/jest-dom: Fornisce matcher Jest personalizzati per le asserzioni sui nodi del DOM.
- Jest: Un popolare framework di testing per JavaScript.
- babel-jest: Un trasformatore Jest che usa Babel per compilare il tuo codice.
- @babel/preset-env: Un preset Babel che determina i plugin e i preset Babel necessari per supportare i tuoi ambienti di destinazione.
- @babel/preset-react: Un preset Babel per React.
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:
- `render`: Questa funzione renderizza il componente nel DOM.
- `screen`: Questo oggetto fornisce metodi per interrogare il DOM.
- `getByText`: Questo metodo trova un elemento tramite il suo contenuto testuale. Il flag `/i` rende la ricerca case-insensitive.
- `expect`: Questa funzione viene utilizzata per fare asserzioni sul comportamento del componente.
- `toBeInTheDocument`: Questo matcher asserisce che l'elemento è presente nel DOM.
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:
- `renderHook`: Questa funzione renderizza l'Hook e restituisce un oggetto contenente il risultato dell'Hook.
- `act`: Questa funzione viene utilizzata per avvolgere qualsiasi codice che provochi aggiornamenti di stato. Ciò garantisce che React possa raggruppare ed elaborare correttamente gli aggiornamenti.
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:
- `jest.mock('../api/dataService')`: Questa riga effettua il mock del modulo `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Dati mockati!' })`: Questa riga configura la funzione `fetchData` mockata affinché restituisca una Promise che si risolve con i dati specificati.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Questa riga asserisce che la funzione `fetchData` mockata è stata chiamata una volta.
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:
- Avvolgiamo il `MyComponent` in `ThemeProvider` per fornire il contesto necessario durante il test.
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:
- Avvolgiamo il `MyComponent` in `MemoryRouter` per fornire un contesto Router fittizio.
- Asseriamo che l'elemento link abbia l'attributo `href` corretto.
Best Practice per Scrivere Test Efficaci
Ecco alcune best practice da seguire quando si scrivono test con RTL:
- Concentrarsi sulle Interazioni dell'Utente: Scrivere test che simulano come gli utenti interagiscono con la tua applicazione.
- Evitare di Testare i Dettagli di Implementazione: Non testare il funzionamento interno dei componenti. Concentrarsi invece sul comportamento osservabile.
- Scrivere Test Chiari e Concisi: Rendere i test facili da capire e da mantenere.
- Usare Nomi di Test Significativi: Scegliere nomi per i test che descrivano accuratamente il comportamento testato.
- Mantenere i Test Isolati: Evitare dipendenze tra i test. Ogni test dovrebbe essere indipendente e auto-contenuto.
- Testare i Casi Limite: Non limitarsi a testare il percorso felice (happy path). Assicurarsi di testare anche i casi limite e le condizioni di errore.
- Scrivere i Test Prima del Codice: Considerare l'uso dello Sviluppo Guidato dai Test (TDD) per scrivere i test prima di scrivere il codice.
- Seguire il Pattern "AAA": Arrange, Act, Assert (Prepara, Agisci, Asserisci). Questo pattern aiuta a strutturare i test e a renderli più leggibili.
- Mantenere i test veloci: I test lenti possono scoraggiare gli sviluppatori dall'eseguirli frequentemente. Ottimizzare i test per la velocità effettuando il mock delle richieste di rete e minimizzando la quantità di manipolazione del DOM.
- Usare messaggi di errore descrittivi: Quando le asserzioni falliscono, i messaggi di errore dovrebbero fornire informazioni sufficienti per identificare rapidamente la causa del fallimento.
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.