Testen API-gesteuerter Komponenten in Storybook
Nutzen Sie MSW in Storybook 10, um API-basierte Komponenten mit Lade-, Fehler-, Leer- und Erfolgszuständen zu testen und Stories in Tests zu verwandeln.
Eine Komponente, die Daten abruft, bleibt in Storybook auf „Loading…” hängen oder wirft einen Fehler, weil kein Backend vorhanden ist, das ihre Anfrage beantwortet – die Lösung besteht darin, diese Anfrage auf der Netzwerkebene mit Mock Service Worker abzufangen, anstatt den Hook zu stubben.
Wenn Sie jemals beobachtet haben, wie eine Komponente in Storybook auf „Loading…” verharrt, ohne dass die Konsole eine Erklärung liefert, liegt die Ursache genau hier: Es ist kein Server vorhanden, der die beim Mounten ausgelöste Anfrage beantwortet. Richten Sie den Mock einmal ein, und jede Story, die Sie schreiben, profitiert automatisch von derselben Behandlung.
Dieser Leitfaden richtet msw-storybook-addon (das MSW 2.x voraussetzt) für Storybook 10 ein, erstellt anschließend eine UserList-Komponente mit vier Stories (Erfolg, Laden, Fehler und leer) und schließt den Kreis, indem diese gemockten Zustände in automatisierte Interaktionstests überführt werden.
Wichtigste Erkenntnisse
- Mocken Sie auf der Netzwerkebene, damit ein Satz MSW-Handler unverändert in Storybook, in Node-basierten Unit-Tests und in Chromatic-Visual-Regression-Tests funktioniert.
- In MSW v2 lautet ein Erfolgs-Handler
http.get(url, () => HttpResponse.json(data));rest.getsowie die Resolver-Signatur(req, res, ctx) => res(ctx.json())existieren nicht mehr. - Modellieren Sie einen dauerhaften Ladezustand durch
await delay('infinite'), einen Fehler mitHttpResponse.json(null, { status: 500 })und ein leeres Ergebnis durch die Rückgabe vonHttpResponse.json([]). - Binden Sie das Addon einmalig ein, indem Sie
mswLoaderdemloaders-Array in.storybook/previewhinzufügen, und stellen Sie den Worker bereit, indem SiestaticDirsin Ihrer Hauptkonfiguration festlegen. - Verknüpfen Sie jeden gemockten Zustand mit einer
play-Funktion, damit der Zustand automatisch geprüft wird. Eine Story mit einerplay-Funktion wird zu einem Komponententest.
Warum schlagen Komponenten mit Datenabruf in Storybook fehl?
Storybook rendert Komponenten isoliert, ohne Application Shell und ohne Server. Eine „App-Komponente”, die beim Mounten fetch, useQuery oder Apollo aufruft, sendet eine Anfrage, die niemand beantwortet – sie verharrt daher entweder dauerhaft in ihrem Ladezustand oder wirft einen Fehler, sobald das Promise abgelehnt wird. Der naheliegende Ansatz ist, den Hook zu stubben: useQuery durch einen Mock ersetzen, der fest kodierte Daten zurückgibt. Tun Sie das nicht. Das Stubben des Hooks koppelt Ihre Story an eine bestimmte Datenbibliothek und an die interne Komponentenstruktur, und der Stub ist wertlos, sobald Sie dasselbe Szenario in einem Node-Test ausführen möchten.
Mocken Sie stattdessen auf der Netzwerkebene. MSW registriert einen Service Worker, der ausgehende Anfragen im Browser abfängt und von Ihnen definierte Antworten zurückgibt – Ihre Komponente durchläuft dabei unverändert ihren echten Datenabruf-Codepfad. Dieselben Handler laufen unter Node über setupServer, was sie portabel über Storybook, Vitest und CI hinweg macht. Sie definieren das Szenario einmal; es funktioniert überall, wo die Komponente ausgeführt wird.
Discover how at OpenReplay.com.
Wie richtet man den Storybook-Mock-API-Stack ein?
Installieren Sie beide Pakete, generieren Sie den Service Worker, registrieren Sie den Loader und verweisen Sie Storybook auf die Worker-Datei. Dieses einmalige Setup folgt Storybooks Leitfaden zum Mocken von Netzwerkanfragen.
npm install msw msw-storybook-addon --save-dev
npx msw init public/
npx msw init public/ schreibt mockServiceWorker.js in Ihr statisches Verzeichnis. Registrieren Sie das Addon global, indem Sie mswLoader zu loaders in .storybook/preview.ts hinzufügen. Loader werden vor dem Rendern einer Story ausgeführt, weshalb das Addon einen Loader statt eines Decorators verwendet:
import type { Preview } from '@storybook/react-vite';
import { initialize, mswLoader } from 'msw-storybook-addon';
initialize();
const preview: Preview = {
loaders: [mswLoader],
};
export default preview;
Stellen Sie anschließend den generierten Worker bereit, indem Sie Ihren Public-Ordner in staticDirs innerhalb von .storybook/main.ts eintragen. Das frühere Flag start-storybook -s public existiert in Storybook 10 nicht mehr:
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;
Die Erfolgs-Story
Hier ist die zu testende Komponente: eine UserList, die ein Array abruft und einen von vier UI-Zuständen rendert. Beachten Sie die zugänglichen Attribute role="status" und role="alert", gegen die die Tests später Abfragen stellen.
// 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>
);
}
Legen Sie die Handler pro Story über parameters.msw.handlers fest. In MSW v2 ersetzt http.get das frühere rest.get, und die HttpResponse-Klasse ersetzt die ctx-Hilfsmethoden:
// 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' },
]),
),
],
},
},
};
Alle Zustände modellieren
Der Happy Path ist der Punkt, an dem die meisten Tutorials aufhören – und gleichzeitig die uninteressanteste Story. Der Wert des Mockens auf Netzwerkebene liegt darin, dass ein einziger Handler-Wechsel jeden Zustand erzeugt, den Ihre Komponente annehmen kann. Session-Replays von API-gesteuerten UIs zeigen regelmäßig Zustände, die Entwickler nie als Story erfasst haben: ein Spinner, der sich nie auflöst, weil eine Anfrage hängt, oder eine leere Antwort, die ein defektes Layout statt eines leeren Zustands rendert. Storybook in Kombination mit MSW ist der Ort, an dem Sie genau diese Zustände vor dem Deployment definieren und prüfen.
| Zustand | Handler | Was er nachweist |
|---|---|---|
| Laden | await delay('infinite') vor der Antwort | Die ausstehende UI rendert und blinkt nicht |
| Fehler | HttpResponse.json(null, { status: 500 }) | Der Fehlerzweig behandelt einen 5xx-Fehler |
| Leer | HttpResponse.json([]) | Das Layout für null Ergebnisse ist gestaltet, nicht defekt |
Die delay-Funktion akzeptiert einen 'infinite'-Modus, der die Anfrage dauerhaft ausstehend hält – die zuverlässige Methode, eine Komponente in ihrem Ladezustand einzufrieren. Importieren Sie delay aus msw und verwenden Sie await innerhalb eines asynchronen Resolvers:
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 mockt GraphQL auf dieselbe Weise (graphql.query('AllUsers', () => HttpResponse.json({ data }))), sodass das Muster ohne Änderungen auf Apollo, urql und React Query übertragbar ist.
Vom Betrachten zum Testen
Eine gemockte Story, die man sich nur ansieht, ist Dokumentation; fügen Sie eine play-Funktion hinzu, und sie wird zu einem automatisierbaren Komponententest. Importieren Sie expect aus storybook/test (das aktuelle Modul, das @storybook/test aus Storybook 8 abgelöst hat) und prüfen Sie das gerenderte Ergebnis:
import { expect } from 'storybook/test';
// bei Success:
play: async ({ canvas }) => {
await expect(await canvas.findByText('Ada Lovelace')).toBeInTheDocument();
},
// bei Loading:
play: async ({ canvas }) => {
await expect(canvas.getByRole('status')).toBeInTheDocument();
},
Für die Loading-Story mit unendlicher Verzögerung prüfen Sie lediglich, ob der Spinner vorhanden ist. Warten Sie nicht auf die Auflösung, da die Anfrage designbedingt dauerhaft aussteht. Diese Stories werden über das Vitest-Addon ausgeführt, das sie als Komponententests in Playwrights Chromium-Browser ausführt – aus der Storybook-UI, dem Terminal oder der CI-Umgebung. Da MSW-Handler umgebungsunabhängig sind, stützen dieselben Erfolgs-, Fehler- und Leer-Handler über setupServer auch eigenständige Vitest-Tests, und Chromatic erstellt von jeder Story einen Snapshot für visuelle Regressionstests.
Mocken Sie auf der Netzwerkebene, modellieren Sie alle vier Zustände und hängen Sie an jeden eine play-Funktion: So wird ein Ordner voller Stories zu einer lebendigen Testsuite, die Bugs wie „lädt ewig” oder „defekter Leer-Zustand” erkennt, bevor sie in Produktion gelangen. Beginnen Sie noch heute, indem Sie die Lade-, Fehler- und Leer-Stories zu einer bestehenden App-Komponente hinzufügen. Die Handler, die Sie dort schreiben, sind dieselben, die Ihre Vitest-Tests wiederverwenden werden.
Häufig gestellte Fragen
Warum bleibt meine Komponente in Storybook auf 'Loading…' hängen, obwohl ich msw-storybook-addon installiert habe?
Die Komponente bleibt hängen, weil entweder kein MSW-Handler mit ihrer Anfrage übereinstimmt oder der Loader des Addons nicht eingebunden ist. Stellen Sie sicher, dass Sie mswLoader dem loaders-Array in .storybook/preview hinzugefügt und initialize() aufgerufen haben, dass public mit npx msw init generiert und in staticDirs eingetragen wurde, und dass ein Handler in parameters.msw.handlers exakt mit der URL und der HTTP-Methode der Anfrage übereinstimmt. Eine URL-Abweichung lässt die Anfrage unbehandelt und die Komponente dauerhaft ausstehend.
Was ist der Unterschied zwischen dem Mocken auf der Netzwerkebene mit MSW und dem Stubben des Fetch-Hooks?
Das Mocken auf Netzwerkebene mit MSW fängt die tatsächlich ausgehende Anfrage ab und gibt eine Antwort zurück, sodass die Komponente ihren echten Datenabruf-Codepfad unverändert durchläuft und dieselben Handler im Browser, in Node über setupServer und in Chromatic funktionieren. Das Stubben des Hooks ersetzt useQuery oder fetch durch fest kodierte Daten, was die Story an eine bestimmte Datenbibliothek und an die interne Komponentenstruktur koppelt und in einem Node-Test nicht wiederverwendet werden kann.
Funktioniert msw-storybook-addon mit MSW-v1-Handlern wie rest.get und res(ctx.json())?
Nein. Seit Version 2.0.0 setzt das Addon MSW 2.0.0 oder höher voraus, und MSW v2 hat den rest-Namespace sowie die Resolver-Signatur res(ctx.json()) entfernt. Schreiben Sie Handler mit http.get und der HttpResponse-Klasse um, zum Beispiel http.get(url, () => HttpResponse.json(data)). MSW-v1-Code ist mit dem aktuellen Addon nicht kompatibel und muss anhand des offiziellen MSW-Migrationsleitfadens von 1.x auf 2.x migriert werden.
Warum hängt meine Loading-Story mit unendlicher Verzögerung, wenn sie als Test ausgeführt wird?
Eine Story, die await delay('infinite') verwendet, hält ihre Anfrage designbedingt dauerhaft ausstehend. Eine play-Funktion, die auf die aufgelöste UI wartet, wird daher nie abgeschlossen. Prüfen Sie stattdessen nur, ob die ausstehende UI vorhanden ist – beispielsweise dass der Spinner mit der Rolle status gerendert wird – anstatt auf die Auflösung zu warten. Wenn ein Node- oder Vitest-Test sauber abschließen muss, verwenden Sie für dieses Szenario eine endliche Verzögerung wie delay(1000) anstelle des Infinite-Modus.
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