frontend / react
Tester avec Mock Service Worker (MSW)
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.
| Approche | Fidélité au comportement réel de l'API | Maintenance à grande échelle |
|---|---|---|
Mock manuel de fetch | Faible, diverge facilement | Coûteuse, répétée par test |
| MSW (handlers centralisés) | Élevée, réutilisable en dev aussi | Centralisé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.
// 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" });
}),
];// 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());// 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>;
}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();
});
});// 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éveloppementRé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
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.