
Mocking de modules et de fonctions (Appels API, etc.)
Apprenez à utiliser les puissantes fonctionnalités de mocking de Jest (`jest.fn`, `jest.mock`, `jest.spyOn`) pour isoler vos composants et hooks React de leurs dépendances externes (API, modules, timers).
Pourquoi et quand utiliser le Mocking ?
Lors de l'écriture de tests unitaires ou d'intégration pour vos composants et hooks React, l'objectif est souvent d'isoler l'unité testée. Cependant, ces unités dépendent fréquemment de facteurs externes : appels à des API backend, utilisation de modules utilitaires complexes, interaction avec des API du navigateur (comme `localStorage` ou `fetch`), gestion du temps (`setTimeout`), etc. Si vos tests dépendent de ces éléments externes réels, ils deviennent :
- Lents : Les appels réseau prennent du temps.
- Non déterministes : Les réponses API peuvent varier, les services externes peuvent être indisponibles.
- Difficiles à configurer : Il peut être complexe de mettre le système externe dans l'état exact requis pour un test spécifique.
- Moins ciblés : Un échec pourrait provenir de la dépendance externe plutôt que de l'unité testée elle-même.
Le mocking (ou simulation) est la technique qui consiste à remplacer ces dépendances externes par des versions contrôlées et prévisibles pendant l'exécution des tests. Jest offre des outils puissants pour créer ces "doublures" (mocks), vous permettant de définir leur comportement (ce qu'elles retournent, si elles lèvent des erreurs) et de vérifier comment votre code interagit avec elles.
Fonctions Mock simples avec `jest.fn()`
Le type de mock le plus simple est la fonction mock, créée avec `jest.fn()`. Elle est extrêmement utile pour simuler des fonctions passées en props (callbacks) ou des dépendances simples. Une fonction `jest.fn()` enregistre tous les appels qu'elle reçoit, les arguments passés à chaque appel, et ce qu'elle a retourné.
test('devrait appeler le callback onSubmit avec les bonnes données', () => {
const mockSubmitHandler = jest.fn(); // Crée une fonction mock
render( );
// ... simuler la saisie et le clic sur le bouton de soumission ...
await userEvent.type(screen.getByLabelText(/nom/i), 'Test');
await userEvent.click(screen.getByRole('button', { name: /soumettre/i }));
// Vérifier si le mock a été appelé
expect(mockSubmitHandler).toHaveBeenCalled();
expect(mockSubmitHandler).toHaveBeenCalledTimes(1);
expect(mockSubmitHandler).toHaveBeenCalledWith({ name: 'Test' }); // Vérifier les arguments
});
// On peut aussi définir une valeur de retour ou une implémentation
test('mock avec valeur de retour', () => {
const mockGetUser = jest.fn(() => ({ id: 1, name: 'Mock User' }));
// Ou : mockGetUser.mockReturnValue({ id: 1, name: 'Mock User' });
const user = mockGetUser();
expect(user).toEqual({ id: 1, name: 'Mock User' });
expect(mockGetUser).toHaveBeenCalled();
});Mocking de Modules complets avec `jest.mock()`
Pour remplacer un module entier (comme une bibliothèque externe type `axios`, un module utilitaire interne, ou même une API native comme `fetch`), on utilise `jest.mock('chemin/vers/module')`. Cet appel doit être fait au niveau supérieur de votre fichier de test (en dehors des `describe` ou `test`), car Jest "hoiste" ces appels avant l'exécution du code.
Par défaut, `jest.mock()` remplace toutes les exportations du module par des fonctions `jest.fn()` (auto-mocking). C'est souvent utile, mais vous voudrez généralement fournir votre propre implémentation simulée pour contrôler le comportement du module mocké.
Vous pouvez fournir une implémentation simulée de deux manières principales :
- Factory function (second argument de `jest.mock`) :
import { fetchData } from './dataService'; // Le module à mocker // Mocker le module et fournir une implémentation spécifique jest.mock('./dataService', () => ({ fetchData: jest.fn(), // Remplacer fetchData par un mock })); // Dans un test... test('devrait afficher les données récupérées', async () => { // Important : typer le mock pour l'autocomplétion et la sécurité const mockedFetchData = fetchData as jest.MockedFunction; mockedFetchData.mockResolvedValueOnce(['item1', 'item2']); // Définir ce que le mock retourne render( ); expect(await screen.findByText('item1')).toBeInTheDocument(); expect(mockedFetchData).toHaveBeenCalledTimes(1); }); - Dossier `__mocks__` : Créez un dossier `__mocks__` au même niveau que le module que vous voulez mocker (ou au même niveau que `node_modules` pour les dépendances externes). Placez-y un fichier avec le même nom que le module original. Ce fichier exportera l'implémentation mockée. Jest utilisera automatiquement ce fichier lorsque `jest.mock('module-name')` sera appelé. C'est utile pour des mocks réutilisables à travers plusieurs fichiers de test.
Exemple : Mocker les Appels API (axios)
Le mocking d'appels API est un cas d'usage très fréquent.
import axios from 'axios';
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile'; // Composant qui fetch un profil
// Mocker le module axios
jest.mock('axios');
// Créer une version typée et mockée pour un usage facile
const mockedAxios = axios as jest.Mocked;
test('affiche le nom utilisateur après un fetch réussi', async () => {
const mockUserData = { name: 'John Doe', email: 'john@doe.com' };
// Simuler une réponse réussie pour axios.get
mockedAxios.get.mockResolvedValueOnce({ data: mockUserData });
render( );
// Vérifier l'état de chargement (optionnel)
expect(screen.getByText(/chargement/i)).toBeInTheDocument();
// Attendre que le nom s'affiche (utilisation de findBy*)
const userName = await screen.findByText(mockUserData.name);
expect(userName).toBeInTheDocument();
// Vérifier que l'indicateur de chargement a disparu
expect(screen.queryByText(/chargement/i)).not.toBeInTheDocument();
// Vérifier que axios.get a été appelé avec la bonne URL
expect(mockedAxios.get).toHaveBeenCalledWith('/api/users/123');
});
test('affiche un message d\'erreur en cas d\'échec du fetch', async () => {
const errorMessage = 'Erreur réseau';
// Simuler une erreur réseau
mockedAxios.get.mockRejectedValueOnce(new Error(errorMessage));
render( );
// Attendre l'affichage du message d'erreur
expect(await screen.findByText(/erreur lors du chargement/i)).toBeInTheDocument();
expect(mockedAxios.get).toHaveBeenCalledWith('/api/users/123');
}); Le principe est similaire pour mocker `fetch` global, bien que la syntaxe pour mocker les fonctions globales diffère légèrement (`global.fetch = jest.fn(...)`).
Contrôler le Temps avec `jest.useFakeTimers()`
Si votre code utilise `setTimeout`, `setInterval` ou `Date`, attendre réellement dans vos tests les rendrait lents et non fiables. Jest permet de prendre le contrôle de l'horloge :
jest.useFakeTimers(); // Activer les timers simulés au début du fichier/describe
test('devrait mettre à jour après 1 seconde', () => {
render( ); // Ce composant fait un setTimeout(..., 1000)
expect(screen.queryByText(/mis à jour/i)).not.toBeInTheDocument();
// Avancer l'horloge de Jest
jest.advanceTimersByTime(1000); // Avance de 1000 ms
// Maintenant, la mise à jour devrait être visible
expect(screen.getByText(/mis à jour/i)).toBeInTheDocument();
// Alternative: exécuter tous les timers en attente
// jest.runOnlyPendingTimers();
});
// N'oubliez pas de potentiellement réinitialiser avec jest.useRealTimers(); si nécessaireEspionner avec `jest.spyOn()`
Parfois, vous ne voulez pas remplacer complètement un module ou une fonction, mais simplement espionner ses appels tout en conservant son implémentation originale, ou remplacer temporairement son implémentation pour un test spécifique. C'est là que `jest.spyOn(object, methodName)` est utile.
import * as utils from './utils'; // Module avec une fonction 'doSomething'
test('devrait appeler utils.doSomething', () => {
const doSomethingSpy = jest.spyOn(utils, 'doSomething');
// Par défaut, l'implémentation originale est conservée
render( );
// ... interactions ...
expect(doSomethingSpy).toHaveBeenCalled();
// Important : restaurer le spy après le test pour ne pas affecter les autres
doSomethingSpy.mockRestore();
});
test('devrait mocker temporairement le retour de Math.random', () => {
const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
// ... votre test qui dépend de Math.random() ...
expect(generateRandomId()).toContain('0.5'); // Exemple
mathRandomSpy.mockRestore(); // Essentiel !
});L'avantage de `spyOn` est qu'il est facile à restaurer avec `.mockRestore()`, garantissant qu'il n'affecte pas les autres tests.
Conclusion : Isoler pour mieux tester
Le mocking est une technique indispensable dans la boîte à outils du testeur React. En utilisant judicieusement `jest.fn()`, `jest.mock()`, `jest.useFakeTimers()` et `jest.spyOn()`, vous pouvez isoler efficacement vos composants et hooks, contrôler leurs dépendances, et écrire des tests plus rapides, plus fiables et plus ciblés. La clé est de mocker uniquement ce qui est nécessaire pour isoler l'unité testée, sans tomber dans l'excès de simulation qui pourrait masquer des problèmes d'intégration réels.