Introduction
Dans le monde du développement logiciel, la qualité des tests est cruciale pour assurer la robustesse des applications. En 2024, environ 70% des développeurs estiment que les tests automatisés sont essentiels pour valider les fonctionnalités de leurs projets. Cependant, simuler les interactions avec une API peut s'avérer complexe et fastidieux. Les dépendances externes, les temps de réponse aléatoires et les serveurs instables rendent souvent les tests fragiles et peu fiables.
C'est ici que MSW (Mock Service Worker) entre en jeu. Cet outil open source permet de simuler des appels API de manière simple et efficace, sans avoir besoin de serveurs réels. En interceptant les requêtes réseau au niveau du Service Worker, MSW offre une approche unique qui se distingue nettement des solutions traditionnelles de mocking. Dans cet article, nous allons explorer en profondeur comment et pourquoi utiliser MSW dans vos tests, les étapes détaillées pour l'implémenter, ses avantages par rapport à d'autres outils, des cas d'utilisation avancés, et les erreurs courantes à éviter.
Pourquoi utiliser MSW ?
Avant de plonger dans la technique, il est essentiel de comprendre les problèmes que MSW résout concrètement. Dans un workflow de test classique, vous avez généralement trois options pour gérer les appels API : utiliser un serveur de test réel, remplacer manuellement les fonctions fetch ou axios, ou utiliser des bibliothèques de mocking qui patchent les modules. Chacune de ces approches présente des limites significatives.
Un serveur de test réel introduit de la latence et des dépendances d'infrastructure. Le remplacement manuel de fetch est fragile et ne reflète pas le comportement réel du réseau. Les bibliothèques de mocking traditionnelles, quant à elles, nécessitent souvent de connaître l'implémentation interne du code testé. MSW élimine ces problèmes en interceptant les requêtes au niveau du réseau, ce qui signifie que votre code applicatif fonctionne exactement comme en production.
Avantages principaux de MSW
Contrairement aux autres outils de mocking d'API, MSW fonctionne au niveau du client en interceptant les requêtes réseau via l'API Service Worker dans le navigateur, ou via un intercepteur de requêtes dans Node.js. Cela vous permet d'émuler des comportements réalistes des API directement dans le navigateur ou l'environnement de test.
- Transparence totale : votre code applicatif ne sait pas qu'il communique avec un mock. Les requêtes fetch, axios ou XMLHttpRequest fonctionnent sans modification.
- Réutilisabilité : les mêmes handlers peuvent être utilisés dans les tests unitaires, les tests d'intégration et même pendant le développement local.
- Réponses dynamiques : vous pouvez simuler des délais, des erreurs réseau, des réponses conditionnelles basées sur les paramètres de la requête.
- Compatibilité : MSW fonctionne avec n'importe quel framework (React, Vue, Angular, Svelte) et n'importe quelle bibliothèque de test.
- Écosystème TypeScript : MSW offre un excellent support TypeScript avec des types intégrés pour une meilleure expérience de développement.
// Exemple basique de configuration MSW
import { setupWorker, rest } from 'msw';
const worker = setupWorker(
rest.get('/api/user', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({ username: 'Jordan', email: 'jordan@example.com' })
);
})
);
worker.start();Comparaison détaillée avec d'autres outils
Les autres outils tels que Sinon.js, Jest ou Nock peuvent également être utilisés pour simuler les API, mais ils nécessitent souvent plus de configuration et ne fonctionnent pas aussi bien dans un environnement de test de bout en bout. MSW, en revanche, est conçu pour être léger et ne perturbe pas l'écosystème de test existant. Voici une comparaison détaillée pour vous aider à faire votre choix.
| Outil | Niveau d'interception | Avantages | Inconvénients |
|---|---|---|---|
| MSW | Réseau (Service Worker / intercepteur) | Transparent, réaliste, réutilisable entre navigateur et Node.js | Requiert une compréhension des Service Workers |
| Sinon.js | Module (remplacement de fonctions) | Bonne intégration avec les tests unitaires, API mature | Configuration complexe pour les tests d'intégration, couplage au code |
| Jest (jest.mock) | Module (remplacement de modules) | Populaire, bien documenté, intégré à Jest | Nécessite de connaître l'implémentation, moins adapté pour le E2E |
| Nock | HTTP (interception Node.js uniquement) | Puissant pour les tests backend Node.js | Ne fonctionne pas dans le navigateur |
Comme le montre ce tableau, MSW se distingue par son approche au niveau réseau, ce qui le rend particulièrement adapté aux projets frontend modernes où les tests doivent refléter le comportement réel de l'application.
Comment implémenter MSW étape par étape
Mettre en place MSW dans un projet nécessite quelques étapes bien définies. Suivez ce guide pour une intégration complète et fonctionnelle.
Installation et configuration initiale
Pour commencer avec MSW, installez-le comme dépendance de développement via npm ou yarn :
npm install msw --save-dev
# ou avec yarn
yarn add msw --devEnsuite, créez un dossier dédié pour organiser vos mocks. Une structure recommandée est la suivante :
src/
mocks/
handlers.js # Définition des handlers de requêtes
server.js # Configuration pour les tests Node.js
browser.js # Configuration pour le navigateurDéfinir les handlers de requêtes
Les handlers sont le cœur de MSW. Ils définissent quelles requêtes intercepter et quelles réponses renvoyer. Créez le fichier handlers.js avec vos définitions :
// src/mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
// Handler pour récupérer un utilisateur
rest.get('/api/user/:id', (req, res, ctx) => {
const { id } = req.params;
return res(
ctx.status(200),
ctx.json({
id: id,
username: 'Jordan',
email: 'jordan@example.com',
role: 'developer'
})
);
}),
// Handler pour la liste des articles
rest.get('/api/articles', (req, res, ctx) => {
const page = req.url.searchParams.get('page') || 1;
return res(
ctx.status(200),
ctx.json({
articles: [
{ id: 1, title: 'Premier article', status: 'published' },
{ id: 2, title: 'Deuxième article', status: 'draft' }
],
page: Number(page),
totalPages: 5
})
);
}),
// Handler pour créer une ressource
rest.post('/api/articles', async (req, res, ctx) => {
const body = await req.json();
return res(
ctx.status(201),
ctx.json({
id: Math.random().toString(36).substr(2, 9),
...body,
createdAt: new Date().toISOString()
})
);
})
];Configurer le serveur pour les tests Node.js
Pour les tests exécutés dans Node.js (Jest, Vitest), vous devez utiliser setupServer au lieu de setupWorker. Créez le fichier server.js :
// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);Puis configurez le serveur dans votre fichier de setup de tests. Avec Jest, ajoutez ceci dans votre fichier setupTests.js :
// src/setupTests.js
import { server } from './mocks/server';
// Démarre le serveur avant tous les tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Réinitialise les handlers après chaque test
afterEach(() => server.resetHandlers());
// Arrête le serveur après tous les tests
afterAll(() => server.close());Conseil : l'option onUnhandledRequest: 'error' est précieuse car elle vous alerte immédiatement si votre code fait une requête non prévue par vos handlers. Cela évite les faux positifs dans vos tests.
Configurer MSW pour le navigateur
Si vous souhaitez utiliser MSW pendant le développement local pour travailler sans backend, configurez le worker pour le navigateur :
// src/mocks/browser.js
import { setupWorker } from 'msw';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);Puis initialisez-le conditionnellement dans votre point d'entrée :
// src/index.js
if (process.env.NODE_ENV === 'development') {
const { worker } = require('./mocks/browser');
worker.start({
onUnhandledRequest: 'bypass' // Laisse passer les requêtes non mockées
});
}N'oubliez pas de générer le Service Worker avec la commande CLI fournie par MSW :
npx msw init public/ --saveCette commande crée le fichier mockServiceWorker.js dans votre dossier public, nécessaire au fonctionnement de l'interception dans le navigateur.
Exemples de tests concrets avec MSW
Voyons maintenant comment exploiter MSW dans des scénarios de test réels et variés. Ces exemples utilisent React Testing Library, mais les concepts s'appliquent à n'importe quel framework.
Test basique : affichage de données
Voici comment tester un composant qui affiche les informations d'un utilisateur récupérées depuis une API :
import { render, screen, waitFor } from '@testing-library/react';
import App from './App';
test('affiche les informations utilisateur', async () => {
render( );
// Attend que les données soient chargées et affichées
const userElement = await screen.findByText(/Jordan/i);
expect(userElement).toBeInTheDocument();
// Vérifie que l'email est également affiché
expect(screen.getByText(/jordan@example.com/i)).toBeInTheDocument();
});Test avancé : simulation d'erreurs
Un des grands atouts de MSW est la facilité avec laquelle vous pouvez simuler des erreurs. Utilisez server.use() pour surcharger temporairement un handler dans un test spécifique :
import { rest } from 'msw';
import { server } from './mocks/server';
import { render, screen } from '@testing-library/react';
import App from './App';
test('affiche un message d\'erreur quand l\'API échoue', async () => {
// Surcharge le handler uniquement pour ce test
server.use(
rest.get('/api/user/:id', (req, res, ctx) => {
return res(
ctx.status(500),
ctx.json({ message: 'Erreur interne du serveur' })
);
})
);
render( );
const errorMessage = await screen.findByText(/erreur/i);
expect(errorMessage).toBeInTheDocument();
});
test('gère une erreur réseau', async () => {
server.use(
rest.get('/api/user/:id', (req, res, ctx) => {
return res.networkError('Connexion refusée');
})
);
render( );
const errorMessage = await screen.findByText(/connexion/i);
expect(errorMessage).toBeInTheDocument();
});Test avec délai simulé : états de chargement
MSW permet aussi de simuler des délais réseau pour tester les états de chargement de vos composants. C'est particulièrement utile pour vérifier que vos spinners et squelettes de chargement fonctionnent correctement :
import { rest } from 'msw';
import { server } from './mocks/server';
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import UserProfile from './UserProfile';
test('affiche un loader puis les données', async () => {
server.use(
rest.get('/api/user/:id', (req, res, ctx) => {
return res(
ctx.delay(1500), // Simule un délai de 1.5 secondes
ctx.status(200),
ctx.json({ username: 'Jordan', email: 'jordan@example.com' })
);
})
);
render( );
// Vérifie que le loader est affiché
expect(screen.getByText(/chargement/i)).toBeInTheDocument();
// Attend que le loader disparaisse
await waitForElementToBeRemoved(() => screen.queryByText(/chargement/i));
// Vérifie que les données sont affichées
expect(screen.getByText(/Jordan/i)).toBeInTheDocument();
});Test de soumission de formulaire
Tester les requêtes POST est tout aussi simple. Voici un exemple de test pour un formulaire de création d'article :
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CreateArticle from './CreateArticle';
test('crée un article avec succès', async () => {
const user = userEvent.setup();
render( );
// Remplit le formulaire
await user.type(screen.getByLabelText(/titre/i), 'Mon nouvel article');
await user.type(screen.getByLabelText(/contenu/i), 'Le contenu de l\'article');
// Soumet le formulaire
await user.click(screen.getByRole('button', { name: /publier/i }));
// Vérifie le message de succès
await waitFor(() => {
expect(screen.getByText(/article créé/i)).toBeInTheDocument();
});
});Cas d'utilisation avancés
Au-delà des tests basiques, MSW offre des fonctionnalités avancées qui répondent à des besoins plus spécifiques dans les projets d'envergure.
Mocker les API GraphQL
MSW ne se limite pas aux API REST. Il supporte nativement GraphQL, ce qui en fait un outil polyvalent pour les projets modernes :
import { graphql } from 'msw';
export const graphqlHandlers = [
graphql.query('GetUser', (req, res, ctx) => {
const { userId } = req.variables;
return res(
ctx.data({
user: {
id: userId,
username: 'Jordan',
email: 'jordan@example.com'
}
})
);
}),
graphql.mutation('CreateArticle', (req, res, ctx) => {
const { title, content } = req.variables;
return res(
ctx.data({
createArticle: {
id: '123',
title,
content,
createdAt: new Date().toISOString()
}
})
);
})
];Handlers conditionnels et dynamiques
Vous pouvez créer des handlers qui répondent différemment en fonction des paramètres de la requête, simulant ainsi des scénarios métier complexes :
rest.get('/api/search', (req, res, ctx) => {
const query = req.url.searchParams.get('q');
const limit = Number(req.url.searchParams.get('limit')) || 10;
if (!query) {
return res(
ctx.status(400),
ctx.json({ error: 'Le paramètre de recherche est requis' })
);
}
if (query === 'empty') {
return res(
ctx.status(200),
ctx.json({ results: [], total: 0 })
);
}
return res(
ctx.status(200),
ctx.json({
results: Array.from({ length: limit }, (_, i) => ({
id: i + 1,
title: `Résultat ${i + 1} pour "${query}"`
})),
total: 42
})
);
})Pièges et erreurs courantes à éviter
Même si MSW simplifie considérablement le mocking d'API, certaines erreurs reviennent fréquemment chez les développeurs qui débutent avec l'outil. Voici les pièges les plus courants et comment les éviter.
Erreurs de cycle de vie du serveur
- Ne pas démarrer le serveur avant les tests : sans l'appel à server.listen() dans le beforeAll, les requêtes ne seront pas interceptées et vos tests échoueront avec des erreurs réseau.
- Oublier de réinitialiser les handlers : si vous utilisez server.use() pour surcharger un handler dans un test, appelez toujours server.resetHandlers() dans le afterEach pour éviter que la surcharge n'affecte les tests suivants.
- Ne pas arrêter le serveur : l'appel à server.close() dans le afterAll est essentiel pour nettoyer proprement les ressources et éviter les fuites mémoire.
Erreurs de configuration
- Confondre setupWorker et setupServer : setupWorker est destiné au navigateur, setupServer est destiné à Node.js. Utiliser le mauvais dans le mauvais contexte provoquera des erreurs silencieuses.
- Oublier le fichier mockServiceWorker.js : pour le mode navigateur, ce fichier doit être généré et placé dans le dossier public. Sans lui, MSW ne pourra pas intercepter les requêtes.
- Ignorer les requêtes non gérées : utilisez l'option onUnhandledRequest: 'error' pour détecter immédiatement les requêtes que vos handlers ne couvrent pas.
Erreurs de logique métier
- Mocks trop permissifs : des handlers qui renvoient toujours un succès ne testent pas les chemins d'erreur. Créez systématiquement des tests pour les cas d'échec.
- Données trop éloignées de la réalité : utilisez des données qui ressemblent à vos vraies réponses API. Idéalement, partagez les types TypeScript entre votre API et vos handlers MSW.
- Ignorer les erreurs de syntaxe dans les handlers : une faute dans un handler peut fausser silencieusement les résultats de test. Utilisez TypeScript pour bénéficier de la vérification de types.
Bonnes pratiques pour un usage optimal
Pour tirer le maximum de MSW dans vos projets, voici un ensemble de bonnes pratiques éprouvées par la communauté.
Organiser ses handlers par domaine
Dans un projet de grande taille, regroupez vos handlers par domaine fonctionnel pour maintenir la lisibilité :
// src/mocks/handlers/user.js
export const userHandlers = [/* ... */];
// src/mocks/handlers/articles.js
export const articleHandlers = [/* ... */];
// src/mocks/handlers/index.js
import { userHandlers } from './user';
import { articleHandlers } from './articles';
export const handlers = [
...userHandlers,
...articleHandlers
];Créer des factories de données
Plutôt que de coder en dur les données dans chaque handler, créez des factories réutilisables :
// src/mocks/factories.js
let idCounter = 0;
export function createUser(overrides = {}) {
idCounter++;
return {
id: idCounter,
username: `user_${idCounter}`,
email: `user${idCounter}@example.com`,
role: 'reader',
createdAt: new Date().toISOString(),
...overrides
};
}
export function createArticle(overrides = {}) {
idCounter++;
return {
id: idCounter,
title: `Article ${idCounter}`,
content: 'Contenu par défaut',
status: 'draft',
...overrides
};
}Documenter les handlers pour l'équipe
Ajoutez des commentaires clairs dans vos handlers pour que toute l'équipe comprenne les scénarios simulés. Un handler bien documenté facilite la maintenance et l'ajout de nouveaux tests par d'autres membres de l'équipe.
Conclusion
MSW est un outil puissant et mature pour simuler les API dans vos tests, permettant de créer des scénarios réalistes sans dépendre de serveurs réels. Son approche par interception réseau le distingue fondamentalement des solutions de mocking traditionnelles et en fait un allié précieux pour tout projet frontend moderne. Voici les points essentiels à retenir :
- MSW intercepte au niveau réseau, rendant le mocking transparent pour votre code applicatif.
- Un même jeu de handlers peut servir pour les tests, le développement local et les démonstrations.
- Le support REST et GraphQL couvre la majorité des architectures API modernes.
- La gestion des erreurs et des délais permet de tester tous les chemins critiques de votre application.
- Une bonne organisation des handlers et l'utilisation de factories garantissent la maintenabilité à long terme.
- Le cycle de vie du serveur (listen, resetHandlers, close) doit être rigoureusement respecté dans vos fichiers de setup.
- TypeScript renforce la fiabilité de vos handlers et réduit les erreurs silencieuses.
Pour mettre en pratique ces concepts, commencez par intégrer MSW dans un projet existant avec quelques handlers simples. Ajoutez ensuite progressivement des scénarios d'erreur, des réponses dynamiques et une structure de fichiers organisée. Vous constaterez rapidement que vos tests deviennent plus fiables, plus rapides et surtout plus proches du comportement réel de votre application en production.