
Tests asynchrones
Apprenez à tester efficacement le code asynchrone (appels API, timers) dans vos composants React en utilisant async/await, findBy*, waitFor et les mocks Jest.
La nature asynchrone des applications React
Les applications React modernes sont intrinsèquement asynchrones. De nombreuses opérations ne se terminent pas immédiatement : récupération de données depuis une API, exécution de timers avec `setTimeout` ou `setInterval`, animations, ou même des mises à jour d'état qui peuvent être traitées par lots par React. Lorsque vous testez des composants impliqués dans de telles opérations, vos tests doivent être capables de gérer cette asynchronité.
Si un test effectue une assertion immédiatement après avoir déclenché une action asynchrone (comme un clic qui lance un appel API), l'assertion échouera probablement car le résultat attendu (par exemple, l'affichage des données récupérées) n'aura pas encore eu lieu. Il est donc crucial de savoir comment faire "attendre" le test jusqu'à ce que l'opération asynchrone soit terminée et que l'interface utilisateur se soit mise à jour.
Support natif de Jest : Promesses et `async/await`
Jest dispose d'un support intégré pour gérer le code asynchrone, en particulier celui basé sur les Promesses. Si une fonction de test (`test` ou `it`) retourne une Promesse, Jest attendra automatiquement que cette Promesse soit résolue avant de considérer le test comme terminé. Si la Promesse est rejetée, le test échouera.
La manière la plus claire et la plus courante de travailler avec les Promesses dans les tests Jest est d'utiliser la syntaxe `async/await`. Marquez simplement votre fonction de test avec le mot-clé `async`, et vous pourrez ensuite utiliser `await` devant toute fonction retournant une Promesse (y compris les utilitaires asynchrones de RTL comme `findBy*` ou `userEvent`).
// Exemple simple non-UI pour illustrer Jest
function fetchData() {
return new Promise(resolve => setTimeout(() => resolve('données'), 100));
}
// Marquez la fonction de test avec 'async'
test('fetchData devrait retourner des données', async () => {
// Utilisez 'await' pour attendre la résolution de la Promesse
const data = await fetchData();
expect(data).toBe('données');
});L'utilisation de `async/await` rend le code de test asynchrone presque aussi lisible que du code synchrone et constitue la pratique standard pour les tests modernes.
Utilitaires asynchrones de React Testing Library : `findBy*` et `waitFor`
Pour les tests d'interface utilisateur avec React, l'asynchronisme se manifeste souvent par des mises à jour du DOM qui se produisent après un certain délai. RTL fournit deux utilitaires principaux pour gérer cela :
- Requêtes `findBy*` : Comme vu précédemment, les requêtes `findByRole`, `findByText`, `findByTestId`, etc., retournent une Promesse. Cette Promesse se résout lorsque l'élément correspondant est trouvé dans le DOM. RTL réessaie automatiquement la requête à intervalles réguliers pendant un délai configurable (par défaut 1000ms) avant d'échouer (rejet de la Promesse). C'est l'outil idéal pour attendre qu'un élément apparaisse à l'écran suite à une opération asynchrone.
test('devrait afficher le nom utilisateur après le chargement', async () => {
render( ); // Ce composant fetch des données
// Attend que l'élément avec le texte 'Nom: Alice' apparaisse
const userNameElement = await screen.findByText(/Nom: Alice/i);
// L'assertion n'est faite qu'après la résolution de findByText
expect(userNameElement).toBeInTheDocument();
});- `waitFor(callback, options?)` : C'est un utilitaire plus générique qui attend qu'une fonction `callback` fournie ne lève plus d'erreur. `waitFor` exécute le callback à plusieurs reprises jusqu'à ce qu'il réussisse (ne lève pas d'erreur) ou que le délai d'attente soit dépassé. Le callback contient généralement une ou plusieurs assertions ou des requêtes `getBy*`/`queryBy*`. `waitFor` est utile pour attendre des conditions plus complexes, comme la disparition d'un élément (en utilisant `queryBy*` dans le callback) ou la vérification de plusieurs conditions simultanément.
test('devrait masquer l\'indicateur de chargement après le fetch', async () => {
render( );
expect(screen.getByText(/Chargement.../i)).toBeInTheDocument(); // Vérifier l'état initial
// Attendre que l'indicateur de chargement disparaisse
await waitFor(() => {
expect(screen.queryByText(/Chargement.../i)).not.toBeInTheDocument();
});
// On peut aussi attendre plusieurs conditions
await waitFor(() => {
expect(screen.queryByText(/Chargement.../i)).not.toBeInTheDocument();
expect(screen.getByText(/Données chargées/i)).toBeInTheDocument();
});
});Le rôle implicite de `act()`
Vous rencontrerez peut-être des avertissements concernant `act()` dans vos tests React. La fonction `act()` de React garantit que toutes les mises à jour d'état et les effets déclenchés à l'intérieur de son scope sont entièrement traités et appliqués au DOM avant que le code suivant ne s'exécute. C'est essentiel pour s'assurer que vous testez l'état final après une mise à jour.
La bonne nouvelle est que les utilitaires clés de React Testing Library, tels que `render`, `fireEvent`, `userEvent` (la plupart des méthodes), `findBy*`, et `waitFor`, sont déjà automatiquement enveloppés dans `act()`. Par conséquent, si vous utilisez ces API RTL comme prévu, vous n'avez généralement pas besoin d'envelopper manuellement votre code dans `act()`.
Les avertissements `act()` apparaissent le plus souvent lorsque des mises à jour d'état se produisent en dehors de ces appels (par exemple, une réponse d'API mockée qui met directement à jour l'état sans être attendue correctement par `findBy*` ou `waitFor`). L'utilisation correcte des utilitaires asynchrones de RTL résout la plupart de ces problèmes.
Exemple concret : Test d'un composant de Fetching
Combinons plusieurs concepts pour tester un composant qui affiche des données après un appel API mocké :
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import axios from 'axios';
import DataFetcher from './DataFetcher';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked;
test('affiche les données après le fetch et masque le chargement', async () => {
// 1. Configurer le mock AVANT le rendu
const fetchedData = { message: 'Données OK' };
mockedAxios.get.mockResolvedValueOnce({ data: fetchedData });
// 2. Rendre le composant
render( );
// 3. Vérifier l'état de chargement initial (synchrone)
expect(screen.getByText(/Chargement des données.../i)).toBeInTheDocument();
// 4. Utiliser findBy* pour attendre l'affichage des données (asynchrone)
const dataElement = await screen.findByText(fetchedData.message);
expect(dataElement).toBeInTheDocument();
// 5. Utiliser waitFor et queryBy* pour vérifier la disparition du chargement (asynchrone)
await waitFor(() => {
expect(screen.queryByText(/Chargement des données.../i)).not.toBeInTheDocument();
});
// 6. Vérifier que l'API a été appelée (synchrone après await)
expect(mockedAxios.get).toHaveBeenCalledWith('/api/data');
expect(mockedAxios.get).toHaveBeenCalledTimes(1);
}); Pièges courants et bonnes pratiques
- Toujours utiliser `async/await` : Marquez vos fonctions `test` avec `async` lorsque vous utilisez `findBy*`, `waitFor`, ou des méthodes `userEvent` asynchrones.
- Ne pas utiliser `getBy*` pour attendre : N'utilisez pas `getBy*` si l'élément n'est pas encore là, cela ferait échouer le test immédiatement. Utilisez `findBy*`.
- Ne pas utiliser `queryBy*` pour attendre l'apparition : `queryBy*` ne réessaie pas et retournera `null` immédiatement si l'élément n'est pas là. Utilisez `findBy*`.
- Eviter les `setTimeout` dans les tests : N'introduisez jamais de `setTimeout` arbitraires dans vos tests pour attendre. Utilisez les utilitaires de RTL (`findBy*`, `waitFor`) ou les timers simulés de Jest (`jest.useFakeTimers`).
- Configurer les mocks avant `render` : Assurez-vous que vos mocks (API, etc.) sont configurés avant d'appeler `render`, sinon le composant pourrait déclencher l'appel réel ou une version non mockée.
En maîtrisant `async/await` et les utilitaires `findBy*` et `waitFor` de RTL, vous pouvez écrire des tests fiables et robustes pour les aspects asynchrones de vos composants React.