Les tests de composants en pratique

Les tests de composants sont un bon point de départ pour montrer du code de test pratique. Les tests de composants sont plus importants que les tests unitaires simples, moins complexes que les tests de bout en bout et illustrent l'interaction avec le DOM. D'un point de vue plus philosophique, l'utilisation de React a permis aux développeurs Web de considérer plus facilement les sites Web ou les applications Web comme des composants.

Par conséquent, tester des composants individuels, quelle que soit leur complexité, est un bon moyen de commencer à penser à tester une application nouvelle ou existante.

Cette page explique comment tester un petit composant avec des dépendances externes complexes. Il est facile de tester un composant qui n'interagit avec aucun autre code, par exemple en cliquant sur un bouton et en confirmant qu'un nombre augmente. En réalité, très peu de code est comme ça, et tester du code qui n'a pas d'interactions peut être d'une valeur limitée.

Composant testé

Nous utilisons Vitest et son environnement JSDOM pour tester un composant React. Cela nous permet d'exécuter rapidement des tests à l'aide de Node sur la ligne de commande tout en émulant un navigateur.

Liste de noms avec un bouton "Sélectionner" à côté de chaque nom
Petit composant React qui affiche une liste d'utilisateurs du réseau.

Ce composant React nommé UserList extrait une liste d'utilisateurs du réseau et vous permet de sélectionner l'un d'entre eux. La liste des utilisateurs est obtenue à l'aide de fetch dans un useEffect, et le gestionnaire de sélection est transmis par Context. Voici son code:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

Cet exemple ne présente pas les bonnes pratiques React (par exemple, il utilise fetch dans useEffect), mais votre codebase est susceptible de contenir de nombreux cas comme celui-ci. Plus précisément, ces cas peuvent sembler trop tenaces à tester au premier coup d'œil. Une prochaine section de ce cours traitera en détail de l'écriture de code testable.

Voici ce que nous testons dans cet exemple :

  • Vérifiez qu'un DOM correct est créé en réponse aux données du réseau.
  • Vérifiez que le clic sur un utilisateur déclenche un rappel.

Chaque composant est différent. Pourquoi est-il intéressant de tester ce cas ?

  • Il utilise le fetch global pour demander des données réelles au réseau, qui peuvent être incohérentes ou lentes lors des tests.
  • Il importe une autre classe, UserRow, que nous ne souhaitons peut-être pas tester implicitement.
  • Il utilise un Context qui ne fait pas spécifiquement partie du code testé et qui est normalement fourni par un composant parent.

Écrire un test rapide pour commencer

Nous pouvons rapidement tester quelque chose de très basique concernant ce composant. Précisons que cet exemple n'est pas très utile. Toutefois, il est utile de configurer le modèle dans un fichier homologue appelé UserList.test.tsx (n'oubliez pas que les outils d'exécution de test tels que Vitest exécutent par défaut des fichiers se terminant par .test.js ou un élément similaire, y compris .tsx):

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

Ce test affirme que lorsque le composant s'affiche, il contient le texte "Utilisateurs". Cela fonctionne même si le composant a pour effet secondaire d'envoyer un fetch au réseau. Le fetch est toujours en cours à la fin du test, sans point de terminaison défini. Nous ne pouvons pas confirmer qu'aucune information utilisateur s'affiche à la fin du test, du moins pas sans attendre un délai d'inactivité.

Simulation fetch()

La simulation consiste à remplacer une fonction ou une classe réelle par un élément que vous contrôlez pour un test. Il s'agit d'une pratique courante dans presque tous les types de tests, à l'exception des tests unitaires les plus simples. Pour en savoir plus, consultez la section Assertions et autres primitives.

Vous pouvez simuler fetch() pour votre test afin qu'il se termine rapidement et renvoie les données attendues, et non des données "réelles" ou inconnues. fetch est un élément global, ce qui signifie que nous n'avons pas besoin de le import ni de require dans notre code.

Dans vitest, vous pouvez simuler un élément global en appelant vi.stubGlobal avec un objet spécial renvoyé par vi.fn(). Cela crée une simulation que nous pouvons modifier ultérieurement. Ces méthodes sont examinées plus en détail dans une section ultérieure de ce cours, mais vous pouvez les voir en pratique dans le code suivant:

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

Ce code ajoute un modèle, décrit une fausse version de la récupération réseau Response, puis attend qu'elle s'affiche. Si le texte n'apparaît pas (vous pouvez le vérifier en remplaçant la requête dans queryByText par un nouveau nom), le test échoue.

Cet exemple utilise les assistants de simulation intégrés de Vitest, mais d'autres frameworks de test ont des approches similaires à la simulation. Vitest est unique en ce sens que vous devez appeler vi.unstubAllGlobals() après tous les tests ou définir une option globale équivalente. Sans "annuler" notre travail, le modèle fetch peut affecter d'autres tests, et chaque requête sera traitée avec notre pile étrange de JSON.

Importations fictives

Vous avez peut-être remarqué que notre composant UserList importe lui-même un composant appelé UserRow. Bien que nous n'ayons pas inclus son code, vous pouvez voir qu'il affiche le nom de l'utilisateur: le test précédent recherche "Sam", qui n'est pas affiché directement dans UserList. Il doit donc provenir de UserRow.

Organigramme illustrant la façon dont les noms des utilisateurs transitent dans notre composant.
UserListTest n'a pas la visibilité de UserRow.

Toutefois, UserRow peut lui-même être un composant complexe. Il peut extraire d'autres données utilisateur ou avoir des effets secondaires qui ne sont pas pertinents pour notre test. En supprimant cette variabilité, vos tests sont plus utiles, en particulier lorsque les composants que vous souhaitez tester deviennent plus complexes et plus enchevêtrés avec leurs dépendances.

Heureusement, vous pouvez utiliser Vitest pour simuler certaines importations, même si votre test ne les utilise pas directement, afin que tout code qui les utilise soit fourni avec une version simple ou connue:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

Comme pour simuler le global fetch, il s'agit d'un outil puissant, mais il peut devenir intenable si votre code comporte de nombreuses dépendances. Encore une fois, la meilleure solution consiste à écrire du code testable.

Cliquer et fournir du contexte

React et d'autres bibliothèques telles que Lit utilisent un concept appelé Context. L'exemple de code inclut UserContext, qui appelle la méthode si un utilisateur est sélectionné. Cette approche est souvent considérée comme une alternative au "forage de propriétés", où le rappel est transmis directement à UserList.

Notre atelier de test n'a pas fourni UserContext. Si vous ajoutez une action de clic au test React sans elle, cela peut, dans le pire des cas, entraîner un plantage du test. Au mieux, si une instance par défaut a été fournie ailleurs, cela peut entraîner un comportement hors de notre contrôle (semblable à un UserRow inconnu ci-dessus).

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

À la place, lorsque vous affichez le composant, vous pouvez fournir votre propre Context. Cet exemple utilise une instance de vi.fn(), une fonction fictive Vitest, qui peut être utilisée pour vérifier qu'un appel a été effectué et quels arguments il a utilisés.

Dans notre cas, cela interagit avec l'fetch simulée dans l'exemple précédent, et le test peut confirmer que l'ID transmis était sam:

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

Il s'agit d'un modèle simple, mais puissant, qui peut vous permettre de supprimer les dépendances non pertinentes du composant principal que vous essayez de tester.

En résumé

Cet exemple montre comment créer un test de composant pour tester et protéger un composant React difficile à tester. Ce test visait à s'assurer que le composant interagit correctement avec ses dépendances: le global fetch, un sous-composant importé et un Context.

Vérifier vos connaissances

Quelles approches ont été utilisées pour tester le composant React ?

Injection de dépendances à l'aide de Context
Simulation des variables globales
Simuler des dépendances complexes par des dépendances simples pour les tests
Vérifier qu'un nombre a augmenté