Retour au cours

frontend / react

Tester avec Mock Service Worker (MSW)

Leçon 301 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi mocker fetch à la main devient fragile à grande échelle
  • Décrire des réponses simulées réalistes avec des handlers MSW
  • Démarrer et réinitialiser le serveur de mock proprement entre les tests
  • Configurer onUnhandledRequest: "error" pour détecter une requête non prévue
  • Surcharger ponctuellement un handler pour un seul test avec server.use(...)

Dans quel contexte ?

La suite de tests de src/components/CourseList.jsx mocke global.fetch manuellement dans chaque fichier de test, avec des réponses simplifiées qui ont fini par diverger du format réel renvoyé par l'API /api/cours (un champ renommé côté backend n'a jamais été répercuté dans les mocks). Les tests passent au vert alors que le composant est cassé en production. Migrer vers MSW, où les handlers interceptent les vraies requêtes réseau avec un format fidèle à l'API, réduit drastiquement ce genre d'écart entre tests et réalité.

Étape 1 : la limite du mock artisanal

Dans la leçon sur React Testing Library, tester un composant qui appelle une API impliquait de remplacer global.fetch par une fonction simulée, à la main. Cette approche fonctionne bien pour un cas isolé.

Étape 2 : pourquoi ça se dégrade à grande échelle

Elle devient vite fragile et répétitive dès qu'une application contient de nombreux appels réseau différents. Chaque test doit reconfigurer sa propre simulation, avec le risque qu'elle s'éloigne peu à peu du comportement réel de l'API.

Étape 3 : un changement de niveau d'interception

Mock Service Worker (MSW) change complètement d'approche. Plutôt que de remplacer fetch dans le code du test, il intercepte les vraies requêtes réseau au niveau du système, de façon totalement transparente pour le code applicatif.

Étape 4 : ce que ça change pour le composant testé

Le composant continue d'appeler fetch("/api/cours") exactement comme en production, sans jamais savoir qu'il parle en réalité à un serveur simulé.

Étape 5 : pourquoi cette fidélité compte

Les réponses simulées sont décrites sous forme de "handlers" qui reproduisent le comportement d'une vraie route d'API, avec ses paramètres d'URL, ses codes de statut, ses erreurs possibles. L'écart entre ce qui est testé et ce qui se passe réellement en production s'en trouve fortement réduit.

Étape 6 : un bonus appréciable

Ces mêmes handlers peuvent aussi être réutilisés directement en développement local, via msw/browser, ce qui garantit des données simulées cohérentes partout, pas seulement dans les tests.

Deux réflexes à adopter

Il reste deux bonnes pratiques à retenir. Configurer onUnhandledRequest: "error" fait échouer un test dès qu'une requête non prévue part réellement sur le réseau, ce qui évite les faux positifs silencieux. Et server.use(...) permet de surcharger ponctuellement un handler pour un seul test, avant de revenir automatiquement aux handlers par défaut ensuite.

Bonne pratique

Toujours configurer onUnhandledRequest: "error" plutôt que "warn" ou l'option par défaut : un test qui laisse partir une vraie requête réseau sans le savoir peut sembler passer alors qu'il ne teste rien de fiable, ou pire, dépendre d'un service externe indisponible en CI.

ApprocheFidélité au comportement réel de l'APIMaintenance à grande échelle
Mock manuel de fetchFaible, diverge facilementCoûteuse, répétée par test
MSW (handlers centralisés)Élevée, réutilisable en dev aussiCentralisée, un seul endroit à maintenir

Ce cours React touche ici à sa fin : des bases de JSX jusqu'aux internals et aux tests, vous disposez maintenant d'une vision complète pour construire des applications modernes, robustes et bien testées.

Commandes & code

Tester avec Mock Service Worker (MSW)

Intercepter les vraies requêtes réseau au niveau du fetch/XHR, plutôt que mocker chaque fonction d'API à la main.

js
// handlers.js : décrit les réponses simulées, au plus près du comportement réel de l'API
import { http, HttpResponse } from "msw";

export const handlers = [
    http.get("/api/cours", () => {
        return HttpResponse.json([
            { id: 1, titre: "HTML" },
            { id: 2, titre: "CSS" },
        ]);
    }),

    http.post("/api/cours/:id/inscription", async ({ params, request }) => {
        const body = await request.json();
        // params.id : segment dynamique de l'URL interceptée
        return HttpResponse.json({ id: params.id, inscrit: true, ...body }, { status: 201 });
    }),

    http.get("/api/cours/:id", ({ params }) => {
        if (params.id === "999") {
            return new HttpResponse(null, { status: 404 }); // simuler un cas d'erreur réaliste
        }
        return HttpResponse.json({ id: params.id, titre: "HTML" });
    }),
];
js
// setupTests.js (Vitest/Jest) : démarre le serveur de mock pour toute la suite de tests
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
import { afterAll, afterEach, beforeAll } from "vitest";

export const server = setupServer(...handlers);

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
// "error" : toute requête NON mockée fait échouer le test -- évite les faux positifs silencieux

afterEach(() => server.resetHandlers()); // repart des handlers par défaut entre chaque test
afterAll(() => server.close());
jsx
// Le composant testé utilise un vrai fetch() -- MSW l'intercepte de façon transparente,
// sans que le code applicatif ne sache qu'il est testé
function ListeCours() {
    const [cours, setCours] = useState([]);
    const [chargement, setChargement] = useState(true);

    useEffect(() => {
        fetch("/api/cours")
            .then((r) => r.json())
            .then((data) => { setCours(data); setChargement(false); });
    }, []);

    if (chargement) return <p>Chargement...</p>;
    return <ul>{cours.map((c) => <li key={c.id}>{c.titre}</li>)}</ul>;
}
jsx
import { render, screen } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { server } from "./setupTests";
import { describe, it, expect } from "vitest";

describe("ListeCours", () => {
    it("affiche les cours renvoyés par l'API", async () => {
        render(<ListeCours />);

        expect(await screen.findByText("HTML")).toBeInTheDocument();
        expect(screen.getByText("CSS")).toBeInTheDocument();
    });

    it("gère une réponse d'erreur serveur", async () => {
        // override PONCTUEL du handler par défaut, pour CE test uniquement
        server.use(
            http.get("/api/cours", () => {
                return new HttpResponse(null, { status: 500 });
            })
        );

        render(<ListeCours />);

        expect(await screen.findByText(/erreur/i)).toBeInTheDocument();
    });
});
js
// MSW fonctionne aussi côté navigateur (dev/démo), via un Service Worker réel --
// même fichier de handlers réutilisé entre tests et environnement de développement
// browser.js
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);
// worker.start() dans main.jsx, uniquement en développement

Résumé

  • MSW intercepte au niveau réseau (fetch/XHR réels) : le code applicatif ne sait jamais qu'il est mocké.
  • onUnhandledRequest: "error" fait échouer un test dès qu'une requête non prévue part réellement, évitant les faux positifs.
  • server.use(...) surcharge un handler pour un seul test, sans affecter les autres grâce à resetHandlers().
  • Les mêmes handlers servent en test ET en développement local (msw/browser), garantissant des mocks cohérents partout.

Exercices pratiques

1 disponible
1

Mission : réparer la confiance perdue dans les tests de CourseList

Objectif : Migrer une suite de tests de mocks fetch artisanaux vers MSW, puis corriger une pollution de test causée par un override de handler mal nettoyé.

Contexte

Les tests de CourseList.jsx mockent global.fetch manuellement avec { id, titre }. Le backend a renommé ce champ en { id, nom } il y a trois semaines : le composant est cassé en production depuis, mais tous les tests restent verts puisque leurs mocks n'ont jamais été mis à jour. L'équipe migre vers MSW. Un second problème apparaît ensuite : un test qui utilise server.use(...) pour simuler une erreur 500 sur /api/cours fait échouer, de façon incompréhensible, le test suivant dans le même fichier — qui attendait pourtant une réponse normale.

Résoudre l’exercice →