Tester les composants pilotés par API dans Storybook
Utilisez MSW dans Storybook 10 pour tester des composants pilotés par API avec états chargement, erreur, vide et succès, puis en faire des tests.
Un composant qui récupère des données restera bloqué sur « Chargement… » ou lèvera une exception dans Storybook, car aucun backend n’est disponible pour répondre à sa requête — la solution consiste à intercepter cette requête au niveau de la couche réseau avec Mock Service Worker, et non à stubber le hook.
Si vous avez déjà regardé un composant rester bloqué sur « Chargement… » dans Storybook sans qu’aucun message dans la console n’explique la situation, voici la cause : aucun serveur n’est présent pour répondre à la requête émise au montage. Configurez le mock une seule fois, et chaque story que vous écrirez bénéficiera automatiquement du même traitement.
Ce guide explique comment configurer msw-storybook-addon (qui nécessite MSW 2.x) sur Storybook 10, puis comment construire un composant UserList avec quatre stories (succès, chargement, erreur et résultat vide), avant de boucler la boucle en transformant ces états mockés en tests d’interaction automatisés.
Points clés à retenir
- Mockez au niveau de la couche réseau afin qu’un même ensemble de handlers MSW fonctionne sans modification dans Storybook, dans les tests unitaires basés sur Node, et dans la régression visuelle Chromatic.
- Dans MSW v2, un handler de succès s’écrit
http.get(url, () => HttpResponse.json(data));rest.getet la signature de resolver(req, res, ctx) => res(ctx.json())n’existent plus. - Modélisez un état de chargement permanent en attendant
delay('infinite'), une erreur avecHttpResponse.json(null, { status: 500 }), et un résultat vide en retournantHttpResponse.json([]). - Câblez l’addon une seule fois en ajoutant
mswLoaderau tableauloadersdans.storybook/preview, et servez le worker en définissantstaticDirsdans votre configuration principale. - Associez chaque état mocké à une fonction
playafin que l’état soit vérifié automatiquement. Une story dotée d’une fonctionplaydevient un test de composant.
Pourquoi les composants qui récupèrent des données dysfonctionnent-ils dans Storybook ?
Storybook rend les composants de manière isolée, sans shell applicatif ni serveur. Un « composant applicatif » qui appelle fetch, useQuery ou Apollo au montage émet une requête à laquelle rien ne répond : il reste soit indéfiniment sur sa branche de chargement, soit lève une exception lorsque la promesse est rejetée. Le réflexe naturel est de stubber le hook : remplacer useQuery par un mock qui retourne des données figées. Ne le faites pas. Stubber le hook couple votre story à une bibliothèque de données spécifique et à la structure interne du composant, et ce stub devient inutile dès que vous souhaitez exécuter le même scénario dans un test Node.
Mockez plutôt au niveau de la couche réseau. MSW enregistre un service worker qui intercepte les requêtes sortantes dans le navigateur et retourne les réponses que vous définissez, de sorte que votre composant exécute son vrai chemin de récupération de données sans modification. Les mêmes handlers s’exécutent sous Node via setupServer, ce qui les rend portables entre Storybook, Vitest et la CI. Vous rédigez le scénario une seule fois ; il fonctionne partout où le composant s’exécute.
Discover how at OpenReplay.com.
Comment configurer la stack d’API mockée pour Storybook ?
Installez les deux packages, générez le service worker, enregistrez le loader et indiquez à Storybook où trouver le fichier worker. Cette configuration unique suit le guide Storybook sur le mocking des requêtes réseau.
npm install msw msw-storybook-addon --save-dev
npx msw init public/
npx msw init public/ écrit mockServiceWorker.js dans votre répertoire statique. Enregistrez l’addon globalement en ajoutant mswLoader à loaders dans .storybook/preview.ts. Les loaders s’exécutent avant le rendu d’une story, ce qui explique pourquoi l’addon utilise un loader plutôt qu’un decorator :
import type { Preview } from '@storybook/react-vite';
import { initialize, mswLoader } from 'msw-storybook-addon';
initialize();
const preview: Preview = {
loaders: [mswLoader],
};
export default preview;
Servez ensuite le worker généré en listant votre dossier public dans staticDirs dans .storybook/main.ts. L’ancien flag start-storybook -s public n’existe plus dans Storybook 10 :
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
framework: '@storybook/react-vite',
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
staticDirs: ['../public'],
};
export default config;
La story de succès
Voici le composant testé, un UserList qui récupère un tableau et affiche l’une des quatre branches d’interface. Notez les attributs accessibles role="status" et role="alert", sur lesquels les tests s’appuieront ultérieurement pour leurs requêtes.
// UserList.tsx
import { useEffect, useState } from 'react';
type User = { id: number; name: string };
const endpoint = 'https://api.example.com/users';
export function UserList() {
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetch(endpoint)
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then((data) => {
setUsers(data);
setStatus('success');
})
.catch(() => setStatus('error'));
}, []);
if (status === 'loading') return <p role="status">Loading…</p>;
if (status === 'error') return <p role="alert">Something went wrong.</p>;
if (users.length === 0) return <p>No users yet.</p>;
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
Définissez les handlers par story via parameters.msw.handlers. Dans MSW v2, http.get remplace rest.get, et la classe HttpResponse remplace les utilitaires ctx :
// UserList.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { http, HttpResponse, delay } from 'msw';
import { UserList } from './UserList';
const endpoint = 'https://api.example.com/users';
const meta = { component: UserList } satisfies Meta<typeof UserList>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Success: Story = {
parameters: {
msw: {
handlers: [
http.get(endpoint, () =>
HttpResponse.json([
{ id: 1, name: 'Ada Lovelace' },
{ id: 2, name: 'Alan Turing' },
]),
),
],
},
},
};
Modéliser chaque état
Le chemin heureux est là où la plupart des tutoriels s’arrêtent, et c’est pourtant la story la moins intéressante. La valeur du mocking au niveau de la couche réseau réside dans le fait qu’un simple changement de handler suffit à reproduire chaque état que votre composant peut atteindre. Les replays de session sur des interfaces pilotées par API font régulièrement apparaître des états que les développeurs n’ont jamais storyboardés : un spinner qui ne se résout jamais parce qu’une requête est restée en suspens, ou une réponse vide qui produit une mise en page cassée plutôt qu’un état vide dédié. Storybook combiné à MSW est l’endroit où vous rédigez et vérifiez exactement ces états avant qu’ils ne soient mis en production.
| État | Handler | Ce qu’il prouve |
|---|---|---|
| Chargement | await delay('infinite') avant de répondre | L’interface en attente s’affiche sans clignoter |
| Erreur | HttpResponse.json(null, { status: 500 }) | La branche d’erreur gère bien un code 5xx |
| Vide | HttpResponse.json([]) | La mise en page sans résultat est conçue intentionnellement, pas cassée |
La fonction delay accepte un mode 'infinite' qui maintient la requête en attente indéfiniment — c’est le moyen fiable de bloquer un composant sur sa branche de chargement. Importez delay depuis msw et attendez-le dans un resolver asynchrone :
export const Loading: Story = {
parameters: {
msw: {
handlers: [
http.get(endpoint, async () => {
await delay('infinite');
return HttpResponse.json([]);
}),
],
},
},
};
export const Error: Story = {
parameters: {
msw: { handlers: [http.get(endpoint, () => HttpResponse.json(null, { status: 500 }))] },
},
};
export const Empty: Story = {
parameters: {
msw: { handlers: [http.get(endpoint, () => HttpResponse.json([]))] },
},
};
MSW mocke GraphQL de la même manière (graphql.query('AllUsers', () => HttpResponse.json({ data }))), ce qui rend ce pattern applicable à Apollo, urql et React Query sans modification.
Du visionnage au test
Une story mockée que vous vous contentez de regarder n’est que de la documentation ; ajoutez une fonction play et elle devient un test de composant automatisable. Importez expect depuis storybook/test (le module actuel, qui a remplacé @storybook/test de Storybook 8) et vérifiez le résultat rendu :
import { expect } from 'storybook/test';
// sur Success :
play: async ({ canvas }) => {
await expect(await canvas.findByText('Ada Lovelace')).toBeInTheDocument();
},
// sur Loading :
play: async ({ canvas }) => {
await expect(canvas.getByRole('status')).toBeInTheDocument();
},
Pour la story Loading avec délai infini, vérifiez uniquement que le spinner est présent. N’attendez pas la résolution, puisque la requête reste en attente indéfiniment par conception. Ces stories s’exécutent via le Vitest addon, qui les lance en tant que tests de composants dans le navigateur Chromium de Playwright, depuis l’interface Storybook, le terminal ou la CI. Étant donné que les handlers MSW sont indépendants de l’environnement, les mêmes handlers de succès, d’erreur et de résultat vide alimentent un test Vitest autonome via setupServer, et Chromatic capture un snapshot de chaque story pour la régression visuelle.
Mockez au niveau de la couche réseau, modélisez les quatre états, et associez une fonction play à chacun : cela transforme un dossier de stories en une suite de tests vivante qui détecte les bugs de chargement infini et de mise en page vide cassée avant qu’ils n’atteignent la production. Commencez dès aujourd’hui en ajoutant les stories de chargement, d’erreur et de résultat vide à un composant applicatif existant. Les handlers que vous y rédigerez seront les mêmes que ceux réutilisés par vos tests Vitest.
FAQ
Pourquoi mon composant reste-t-il bloqué sur « Chargement… » dans Storybook même après l'installation de msw-storybook-addon ?
Le composant est bloqué parce qu'aucun handler MSW ne correspond à sa requête ou parce que le loader de l'addon n'est pas câblé. Vérifiez que vous avez ajouté mswLoader au tableau loaders dans .storybook/preview et appelé initialize(), que le dossier public a été généré avec npx msw init et listé dans staticDirs, et qu'un handler dans parameters.msw.handlers correspond exactement à l'URL et à la méthode de la requête. Une URL incorrecte laisse la requête sans réponse et le composant en attente indéfiniment.
Quelle est la différence entre mocker au niveau de la couche réseau avec MSW et stubber le hook fetch ?
Le mocking au niveau de la couche réseau avec MSW intercepte la vraie requête sortante et retourne une réponse, de sorte que le composant exécute son vrai chemin de récupération de données sans modification et que les mêmes handlers fonctionnent dans le navigateur, sous Node via setupServer, et dans Chromatic. Stubber le hook remplace useQuery ou fetch par des données figées, ce qui couple la story à une bibliothèque de données spécifique et à la structure interne du composant, et ne peut pas être réutilisé dans un test Node.
msw-storybook-addon est-il compatible avec les handlers MSW v1 comme rest.get et res(ctx.json()) ?
Non. Depuis la version 2.0.0, l'addon requiert MSW 2.0.0 ou supérieur, et MSW v2 a supprimé le namespace rest ainsi que la signature de resolver res(ctx.json()). Réécrivez vos handlers en utilisant http.get et la classe HttpResponse, par exemple http.get(url, () => HttpResponse.json(data)). Le code MSW v1 ne fonctionnera pas avec l'addon actuel et doit être migré en suivant le guide officiel de migration de MSW de la v1.x vers la v2.x.
Pourquoi ma story de chargement avec délai infini se bloque-t-elle lorsqu'elle est exécutée en tant que test ?
Une story utilisant await delay('infinite') maintient sa requête en attente indéfiniment par conception, de sorte qu'une fonction play qui attend la résolution de l'interface ne se terminera jamais. Vérifiez uniquement que l'interface en attente est présente, par exemple que le spinner avec le role status est affiché, plutôt que d'attendre la résolution. Si un test Node ou Vitest doit se terminer proprement, utilisez un délai fini comme delay(1000) au lieu du mode infini pour ce scénario.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k