Maîtrisez React Testing Library (RTL) avec ce guide complet. Écrivez des tests efficaces, maintenables et centrés sur l'utilisateur pour vos applications React.
React Testing Library : Un guide complet
Dans le paysage actuel du développement web, qui évolue rapidement, il est primordial de garantir la qualité et la fiabilité de vos applications React. React Testing Library (RTL) s'est imposée comme une solution populaire et efficace pour écrire des tests axés sur la perspective de l'utilisateur. Ce guide offre un aperçu complet de RTL, couvrant tout, des concepts fondamentaux aux techniques avancées, pour vous permettre de créer des applications React robustes et maintenables.
Pourquoi choisir React Testing Library ?
Les approches de test traditionnelles reposent souvent sur les détails d'implémentation, ce qui rend les tests fragiles et susceptibles de se briser lors de changements mineurs dans le code. RTL, au contraire, vous encourage à tester vos composants comme le ferait un utilisateur, en se concentrant sur ce que l'utilisateur voit et expérimente. Cette approche offre plusieurs avantages clés :
- Tests centrés sur l'utilisateur : RTL favorise l'écriture de tests qui reflètent la perspective de l'utilisateur, garantissant que votre application fonctionne comme prévu du point de vue de l'utilisateur final.
- Réduction de la fragilité des tests : En évitant de tester les détails d'implémentation, les tests RTL sont moins susceptibles de se briser lorsque vous refactorisez votre code, ce qui conduit à des tests plus maintenables et robustes.
- Amélioration de la conception du code : RTL vous encourage à écrire des composants accessibles et faciles à utiliser, ce qui conduit à une meilleure conception globale du code.
- Accent sur l'accessibilité : RTL facilite le test de l'accessibilité de vos composants, garantissant que votre application est utilisable par tous.
- Processus de test simplifié : RTL fournit une API simple et intuitive, ce qui facilite l'écriture et la maintenance des tests.
Mise en place de votre environnement de test
Avant de pouvoir commencer à utiliser RTL, vous devez configurer votre environnement de test. Cela implique généralement l'installation des dépendances nécessaires et la configuration de votre framework de test.
Prérequis
- Node.js et npm (ou yarn) : Assurez-vous que Node.js et npm (ou yarn) sont installés sur votre système. Vous pouvez les télécharger sur le site officiel de Node.js.
- Projet React : Vous devez avoir un projet React existant ou en créer un nouveau en utilisant Create React App ou un outil similaire.
Installation
Installez les paquets suivants en utilisant npm ou yarn :
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Ou, en utilisant yarn :
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Explication des paquets :
- @testing-library/react : La bibliothèque principale pour tester les composants React.
- @testing-library/jest-dom : Fournit des matchers Jest personnalisés pour faire des assertions sur les nœuds du DOM.
- Jest : Un framework de test JavaScript populaire.
- babel-jest : Un transformateur Jest qui utilise Babel pour compiler votre code.
- @babel/preset-env : Un préréglage Babel qui détermine les plugins et préréglages Babel nécessaires pour supporter vos environnements cibles.
- @babel/preset-react : Un préréglage Babel pour React.
Configuration
Créez un fichier `babel.config.js` à la racine de votre projet avec le contenu suivant :
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Mettez à jour votre fichier `package.json` pour inclure un script de test :
{
"scripts": {
"test": "jest"
}
}
Créez un fichier `jest.config.js` à la racine de votre projet pour configurer Jest. Une configuration minimale pourrait ressembler à ceci :
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};
Créez un fichier `src/setupTests.js` avec le contenu suivant. Cela garantit que les matchers DOM de Jest sont disponibles dans tous vos tests :
import '@testing-library/jest-dom/extend-expect';
Écrire votre premier test
Commençons par un exemple simple. Supposons que vous ayez un composant React qui affiche un message de salutation :
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
Maintenant, écrivons un test pour ce composant :
// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('affiche un message de salutation', () => {
render(<Greeting name="World" />);
const greetingElement = screen.getByText(/Hello, World!/i);
expect(greetingElement).toBeInTheDocument();
});
Explication :
- `render` : Cette fonction effectue le rendu du composant dans le DOM.
- `screen` : Cet objet fournit des méthodes pour interroger le DOM.
- `getByText` : Cette méthode trouve un élément par son contenu textuel. L'indicateur `/i` rend la recherche insensible à la casse.
- `expect` : Cette fonction est utilisée pour faire des assertions sur le comportement du composant.
- `toBeInTheDocument` : Ce matcher affirme que l'élément est présent dans le DOM.
Pour exécuter le test, exécutez la commande suivante dans votre terminal :
npm test
Si tout est configuré correctement, le test devrait passer.
Les requêtes courantes de RTL
RTL fournit diverses méthodes de requête pour trouver des éléments dans le DOM. Ces requêtes sont conçues pour imiter la façon dont les utilisateurs interagissent avec votre application.
`getByRole`
Cette requête trouve un élément par son rôle ARIA. C'est une bonne pratique d'utiliser `getByRole` chaque fois que possible, car cela favorise l'accessibilité et garantit que vos tests sont résilients aux changements dans la structure du DOM sous-jacente.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Cette requête trouve un élément par le texte de son label associé. C'est utile pour tester les éléments de formulaire.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Cette requête trouve un élément par son texte de substitution (placeholder).
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Cette requête trouve un élément image par son texte alternatif (alt text). Il est important de fournir un texte alternatif significatif pour toutes les images afin de garantir l'accessibilité.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Cette requête trouve un élément par son attribut `title`.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Cette requête trouve un élément par sa valeur affichée. C'est utile pour tester les champs de formulaire avec des valeurs pré-remplies.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
Requêtes `getAllBy*`
En plus des requêtes `getBy*`, RTL fournit également des requêtes `getAllBy*`, qui retournent un tableau d'éléments correspondants. Celles-ci sont utiles lorsque vous devez affirmer que plusieurs éléments avec les mêmes caractéristiques sont présents dans le DOM.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
Requêtes `queryBy*`
Les requêtes `queryBy*` sont similaires aux requêtes `getBy*`, mais elles retournent `null` si aucun élément correspondant n'est trouvé, au lieu de lever une erreur. C'est utile lorsque vous voulez affirmer qu'un élément n'est *pas* présent dans le DOM.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
Requêtes `findBy*`
Les requêtes `findBy*` sont des versions asynchrones des requêtes `getBy*`. Elles retournent une Promesse (Promise) qui se résout lorsque l'élément correspondant est trouvé. Elles sont utiles pour tester des opérations asynchrones, comme la récupération de données depuis une API.
// Simulation d'une récupération de données asynchrone
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('charge les données de manière asynchrone', async () => {
render(<MyComponent />);
const dataElement = await screen.findByText('Data Loaded!');
expect(dataElement).toBeInTheDocument();
});
Simulation des interactions utilisateur
RTL fournit les API `fireEvent` et `userEvent` pour simuler les interactions de l'utilisateur, telles que cliquer sur des boutons, taper dans des champs de saisie et soumettre des formulaires.
`fireEvent`
`fireEvent` vous permet de déclencher des événements DOM par programmation. C'est une API de plus bas niveau qui vous donne un contrôle précis sur les événements qui sont déclenchés.
<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';
test('simule un clic de bouton', () => {
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` est une API de plus haut niveau qui simule les interactions de l'utilisateur de manière plus réaliste. Elle gère des détails tels que la gestion du focus et l'ordre des événements, rendant vos tests plus robustes et moins fragiles.
<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';
test('simule la saisie dans un champ de texte', () => {
const inputElement = screen.getByRole('textbox');
userEvent.type(inputElement, 'Hello, world!');
expect(inputElement).toHaveValue('Hello, world!');
});
Tester le code asynchrone
De nombreuses applications React impliquent des opérations asynchrones, telles que la récupération de données depuis une API. RTL fournit plusieurs outils pour tester le code asynchrone.
`waitFor`
`waitFor` vous permet d'attendre qu'une condition devienne vraie avant de faire une assertion. C'est utile pour tester des opérations asynchrones qui prennent un certain temps à se terminer.
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('attend que les données soient chargées', async () => {
render(<MyComponent />);
await waitFor(() => screen.getByText('Data loaded!'));
const dataElement = screen.getByText('Data loaded!');
expect(dataElement).toBeInTheDocument();
});
Requêtes `findBy*`
Comme mentionné précédemment, les requêtes `findBy*` sont asynchrones et retournent une Promesse qui se résout lorsque l'élément correspondant est trouvé. Elles sont utiles pour tester des opérations asynchrones qui entraînent des modifications du DOM.
Tester les Hooks
Les Hooks React sont des fonctions réutilisables qui encapsulent une logique avec état. RTL fournit l'utilitaire `renderHook` de `@testing-library/react-hooks` (qui est obsolète au profit de `@testing-library/react` directement depuis la v17) pour tester les Hooks personnalisés de manière isolée.
// 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('incrémente le compteur', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Explication :
- `renderHook` : Cette fonction effectue le rendu du Hook et retourne un objet contenant le résultat du Hook.
- `act` : Cette fonction est utilisée pour envelopper tout code qui provoque des mises à jour d'état. Cela garantit que React peut correctement regrouper et traiter les mises à jour.
Techniques de test avancées
Une fois que vous maîtrisez les bases de RTL, vous pouvez explorer des techniques de test plus avancées pour améliorer la qualité et la maintenabilité de vos tests.
Mocker des modules
Parfois, vous devrez peut-être mocker des modules externes ou des dépendances pour isoler vos composants et contrôler leur comportement pendant les tests. Jest fournit une API de mocking puissante à cet effet.
// 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('récupère les données depuis l\'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);
});
Explication :
- `jest.mock('../api/dataService')` : Cette ligne mocke le module `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })` : Cette ligne configure la fonction `fetchData` mockée pour qu'elle retourne une Promesse qui se résout avec les données spécifiées.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)` : Cette ligne affirme que la fonction `fetchData` mockée a été appelée une fois.
Fournisseurs de contexte (Context Providers)
Si votre composant dépend d'un Fournisseur de contexte, vous devrez envelopper votre composant dans le fournisseur pendant les tests. Cela garantit que le composant a accès aux valeurs du contexte.
// 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('change le thème', () => {
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();
});
Explication :
- Nous enveloppons le `MyComponent` dans `ThemeProvider` pour fournir le contexte nécessaire pendant les tests.
Tester avec le Router
Lorsque vous testez des composants qui utilisent React Router, vous devrez fournir un contexte de Router mocké. Vous pouvez y parvenir en utilisant le composant `MemoryRouter` de `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('affiche un lien vers la page \"à propos\"', () => {
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', '/about');
});
Explication :
- Nous enveloppons le `MyComponent` dans `MemoryRouter` pour fournir un contexte de Router mocké.
- Nous affirmons que l'élément de lien a le bon attribut `href`.
Meilleures pratiques pour écrire des tests efficaces
Voici quelques meilleures pratiques à suivre lors de l'écriture de tests avec RTL :
- Concentrez-vous sur les interactions utilisateur : Écrivez des tests qui simulent la façon dont les utilisateurs interagissent avec votre application.
- Évitez de tester les détails d'implémentation : Ne testez pas le fonctionnement interne de vos composants. Concentrez-vous plutôt sur le comportement observable.
- Écrivez des tests clairs et concis : Rendez vos tests faciles à comprendre et à maintenir.
- Utilisez des noms de test significatifs : Choisissez des noms de test qui décrivent avec précision le comportement testé.
- Gardez les tests isolés : Évitez les dépendances entre les tests. Chaque test doit être indépendant et autonome.
- Testez les cas limites : Ne vous contentez pas de tester le "happy path". Assurez-vous de tester également les cas limites et les conditions d'erreur.
- Écrivez les tests avant de coder : Envisagez d'utiliser le développement piloté par les tests (TDD) pour écrire les tests avant d'écrire votre code.
- Suivez le modèle "AAA" : Arrange, Act, Assert (Organiser, Agir, Affirmer). Ce modèle aide à structurer vos tests et à les rendre plus lisibles.
- Gardez vos tests rapides : Des tests lents peuvent décourager les développeurs de les exécuter fréquemment. Optimisez la vitesse de vos tests en mockant les requêtes réseau et en minimisant la quantité de manipulation du DOM.
- Utilisez des messages d'erreur descriptifs : Lorsque les assertions échouent, les messages d'erreur doivent fournir suffisamment d'informations pour identifier rapidement la cause de l'échec.
Conclusion
React Testing Library est un outil puissant pour écrire des tests efficaces, maintenables et centrés sur l'utilisateur pour vos applications React. En suivant les principes et les techniques décrits dans ce guide, vous pouvez créer des applications robustes et fiables qui répondent aux besoins de vos utilisateurs. N'oubliez pas de vous concentrer sur les tests du point de vue de l'utilisateur, d'éviter de tester les détails d'implémentation et d'écrire des tests clairs et concis. En adoptant RTL et les meilleures pratiques, vous pouvez améliorer considérablement la qualité et la maintenabilité de vos projets React, quel que soit votre emplacement ou les exigences spécifiques de votre public mondial.