Français

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 :

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

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 :

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 :

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 :

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 :

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 :

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 :

Meilleures pratiques pour écrire des tests efficaces

Voici quelques meilleures pratiques à suivre lors de l'écriture de tests avec RTL :

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.