Contactez-nous

Tests de hooks personnalisés (`renderHook`)

Apprenez à isoler et tester la logique de vos hooks personnalisés React en utilisant la fonction `renderHook` de React Testing Library pour des tests unitaires ciblés.

Pourquoi tester les hooks personnalisés isolément ?

Les hooks personnalisés sont un mécanisme puissant dans React pour extraire et réutiliser la logique stateful et les effets de bord entre différents composants. Ils encapsulent souvent une logique métier ou une interaction complexe (gestion d'un état complexe, abonnement à des sources de données, interaction avec des API...). Il est donc crucial de s'assurer que cette logique fonctionne correctement.

Bien qu'on puisse tester un hook personnalisé indirectement en testant un composant qui l'utilise, il est souvent plus efficace et plus ciblé de le tester directement et isolément. Cela permet de :

  • Se concentrer sur la logique du hook : Sans se préoccuper du rendu ou des autres aspects d'un composant qui l'utilise.
  • Tester différents scénarios : Plus facilement fournir différentes configurations initiales ou simuler des séquences d'actions spécifiques au hook.
  • Améliorer la localisation des erreurs : Si un test de hook échoue, on sait que le problème se situe dans la logique du hook lui-même.
  • Augmenter la couverture de test : Tester des cas limites ou des chemins de code spécifiques au hook qui pourraient être difficiles à atteindre via un composant.

React Testing Library fournit un utilitaire dédié à cet effet : la fonction `renderHook`.

La fonction `renderHook` : Comment ça marche ?

Les hooks React ne peuvent être appelés qu'à l'intérieur du corps d'un composant fonctionnel. La fonction `renderHook` de RTL contourne cette limitation en créant un petit composant de test interne spécifiquement pour exécuter votre hook et capturer ses sorties et ses mises à jour.

Elle prend en argument une fonction callback qui appelle et retourne le résultat de votre hook personnalisé. Elle retourne ensuite un objet contenant plusieurs propriétés utiles pour interagir avec le hook et vérifier son comportement.

import { renderHook } from '@testing-library/react';
import useCustomCounter from './useCustomCounter'; // Votre hook personnalisé

test('should increment counter', () => {
  // On appelle renderHook avec une fonction qui utilise le hook
  const { result } = renderHook(() => useCustomCounter());

  // 'result.current' contient la valeur retournée par le hook à l'instant T
  expect(result.current.count).toBe(0);

  // Comment interagir avec le hook ? (voir section suivante)
});

Interagir avec le hook et vérifier les résultats

L'objet retourné par `renderHook` contient notamment :

  • `result` : Un objet dont la propriété `current` contient la dernière valeur retournée par votre hook personnalisé. C'est ce que vous utiliserez principalement pour vos assertions `expect`.
  • `rerender(newProps?)` : Permet de relancer le rendu du composant de test interne, éventuellement avec de nouvelles props si votre hook en accepte (passées via l'option `initialProps`). Utile pour tester la réaction du hook aux changements de props.
  • `unmount()` : Démonte le composant de test, déclenchant ainsi les fonctions de nettoyage (`return` dans `useEffect`) de votre hook. Essentiel pour tester la logique de cleanup.

Pour tester les fonctions retournées par votre hook (par exemple, une fonction pour incrémenter un compteur), vous devez les appeler en dehors du callback de `renderHook`, mais en utilisant la valeur `result.current`. Cependant, si l'appel de ces fonctions provoque une mise à jour de l'état à l'intérieur du hook, cette mise à jour ne sera pas immédiatement reflétée dans `result.current` de manière synchrone. Il faut utiliser un utilitaire comme `act` de React (ou implicitement via `waitFor` de RTL) pour s'assurer que toutes les mises à jour d'état sont traitées avant de faire des assertions.

`act` est une fonction fournie par React (ou React Testing Library qui l'enveloppe) qui garantit que toutes les mises à jour déclenchées à l'intérieur de son callback sont appliquées au DOM (ou dans notre cas, à l'état du hook) avant que `act` ne se termine.

import { renderHook, act } from '@testing-library/react';
import useCustomCounter from './useCustomCounter'; // Ex: hook qui retourne { count, increment }

test('increment function updates the count', () => {
  const { result } = renderHook(() => useCustomCounter());

  expect(result.current.count).toBe(0);

  // On enveloppe l'appel de la fonction du hook dans act
  act(() => {
    result.current.increment();
  });

  // Après act, result.current est mis à jour
  expect(result.current.count).toBe(1);

  // On peut appeler plusieurs fois
  act(() => {
    result.current.increment();
  });
  expect(result.current.count).toBe(2);
});

Tester les hooks avec des props initiales et des effets

Si votre hook personnalisé accepte des arguments (props initiales), vous pouvez les passer via l'option `initialProps` de `renderHook` :

// Hook: function useCounter(initialValue = 0) { ... }

test('should use initial value', () => {
  const { result } = renderHook(
    (props) => useCounter(props.initialValue), // Le callback reçoit les props
    { initialProps: { initialValue: 10 } } // Passer les props ici
  );
  expect(result.current.count).toBe(10);
});

test('should update count when initialValue prop changes', () => {
  const { result, rerender } = renderHook(
    (props) => useCounter(props.initialValue),
    { initialProps: { initialValue: 10 } }
  );

  // Relancer le rendu avec de nouvelles props
  rerender({ initialValue: 20 });

  // Vérifier si le hook a réagi au changement de prop (si applicable)
  // Note: la réaction dépend de l'implémentation du hook (ex: useEffect avec dépendance)
  // expect(result.current.count).toBe(20); // Seulement si le hook gère ce changement
});

Pour tester les `useEffect`, notamment la logique de nettoyage :

// Hook: function useEventListener(eventName, handler, element = window) { 
//   useEffect(() => { element.addEventListener(...); return () => element.removeEventListener(...); }, [...]);
// }

test('should cleanup event listener on unmount', () => {
  const addEventListenerMock = jest.spyOn(window, 'addEventListener');
  const removeEventListenerMock = jest.spyOn(window, 'removeEventListener');
  const handler = jest.fn();

  const { unmount } = renderHook(() => useEventListener('click', handler));

  expect(addEventListenerMock).toHaveBeenCalledWith('click', handler, undefined); // ou options

  // Démonter le composant de test pour déclencher le cleanup
  unmount();

  expect(removeEventListenerMock).toHaveBeenCalledWith('click', handler, undefined);
});

Pour les effets asynchrones (ex: fetch de données), vous utiliserez souvent `waitFor` de RTL pour attendre que l'état du hook soit mis à jour après la résolution de la promesse (assurez-vous de mocker l'appel API).

import { renderHook, waitFor } from '@testing-library/react';
import useFetchData from './useFetchData';
import axios from 'axios';

jest.mock('axios'); // Mocker axios
const mockedAxios = axios as jest.Mocked;

test('should fetch data and update state', async () => {
  mockedAxios.get.mockResolvedValueOnce({ data: { message: 'Success!' } });

  const { result } = renderHook(() => useFetchData('/api/data'));

  expect(result.current.isLoading).toBe(true);

  // Attendre que isLoading devienne false (signe que le fetch est terminé)
  await waitFor(() => expect(result.current.isLoading).toBe(false));

  expect(result.current.data).toEqual({ message: 'Success!' });
  expect(result.current.error).toBeNull();
  expect(mockedAxios.get).toHaveBeenCalledWith('/api/data');
});

Conclusion : Tester la logique réutilisable

La fonction `renderHook` est un outil précieux pour tester unitairement la logique encapsulée dans vos hooks personnalisés. En isolant le hook de tout composant spécifique, vous pouvez écrire des tests plus ciblés, plus robustes et plus faciles à maintenir pour ces briques de logique réutilisables.

N'oubliez pas d'utiliser `act` (ou les utilitaires asynchrones comme `waitFor` qui l'utilisent implicitement) lorsque vous testez des actions qui provoquent des mises à jour d'état dans le hook, et de mocker les dépendances externes pour garantir des tests déterministes et rapides.