Contactez-nous

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 :

  1. 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);
    });
  2. 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écessaire

Espionner 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.