Pruebas de Componentes Basados en API en Storybook
Usa MSW en Storybook 10 para probar componentes con API en estados de carga, error, vacío y éxito, y convertir stories en tests.
Un componente que obtiene datos se quedará colgado en “Loading…” o lanzará un error en Storybook porque no hay ningún backend que responda a su solicitud — la solución es interceptar esa solicitud en la capa de red con Mock Service Worker, no sustituir el hook.
Si alguna vez has visto un componente quedarse en “Loading…” en Storybook sin ningún mensaje en la consola que lo explique, esta es la causa: no hay ningún servidor que responda a la solicitud que lanza al montarse. Configura el mock una sola vez y cada story que escribas recibirá el mismo tratamiento de forma automática.
Esta guía configura msw-storybook-addon (que requiere MSW 2.x) en Storybook 10, luego construye un componente UserList con cuatro stories (éxito, carga, error y vacío), y cierra el ciclo convirtiendo esos estados simulados en pruebas de interacción automatizadas.
Conclusiones Clave
- Realiza los mocks en la capa de red para que un único conjunto de handlers de MSW funcione sin cambios en Storybook, en pruebas unitarias basadas en Node y en regresión visual con Chromatic.
- En MSW v2, un handler de éxito es
http.get(url, () => HttpResponse.json(data)); ya no existerest.getni la firma de resolver(req, res, ctx) => res(ctx.json()). - Modela un estado de carga permanente usando
await delay('infinite'), un error conHttpResponse.json(null, { status: 500 }), y un resultado vacío devolviendoHttpResponse.json([]). - Conecta el addon una sola vez añadiendo
mswLoaderal arrayloadersen.storybook/preview, y sirve el worker configurandostaticDirsen tu configuración principal. - Combina cada estado simulado con una función
playpara que el estado se verifique automáticamente. Una story con una funciónplayse convierte en una prueba de componente.
¿Por qué los componentes que obtienen datos fallan en Storybook?
Storybook renderiza los componentes de forma aislada, sin shell de aplicación ni servidor. Un “componente de aplicación” que llama a fetch, useQuery o Apollo al montarse lanza una solicitud que nada responde, por lo que o bien se queda indefinidamente en su rama de carga, o lanza un error cuando la promesa se rechaza. El instinto es sustituir el hook: reemplazar useQuery por un mock que devuelva datos predefinidos. No lo hagas. Sustituir el hook acopla tu story a una biblioteca de datos concreta y a la estructura interna del componente, y el stub deja de ser útil en el momento en que quieras ejecutar el mismo escenario en una prueba de Node.
En su lugar, realiza los mocks en la capa de red. MSW registra un service worker que intercepta las solicitudes salientes en el navegador y devuelve las respuestas que tú defines, de modo que tu componente ejecuta su código de obtención de datos real sin modificaciones. Los mismos handlers se ejecutan bajo Node mediante setupServer, lo que los hace portables entre Storybook, Vitest y CI. Defines el escenario una sola vez; funciona en cualquier entorno donde se ejecute el componente.
Discover how at OpenReplay.com.
¿Cómo se configura el stack de API mock para Storybook?
Instala ambos paquetes, genera el service worker, registra el loader y apunta Storybook al archivo del worker. Esta configuración única sigue la guía de Storybook para simular solicitudes de red.
npm install msw msw-storybook-addon --save-dev
npx msw init public/
npx msw init public/ escribe mockServiceWorker.js en tu directorio estático. Registra el addon globalmente añadiendo mswLoader a loaders en .storybook/preview.ts. Los loaders se ejecutan antes de que se renderice una story, que es la razón por la que el addon usa un loader en lugar de 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;
Luego sirve el worker generado listando tu carpeta pública en staticDirs dentro de .storybook/main.ts. El antiguo flag start-storybook -s public ya no existe en 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 éxito
Este es el componente bajo prueba, un UserList que obtiene un array y renderiza una de cuatro ramas de interfaz. Observa los atributos accesibles role="status" y role="alert", contra los que las pruebas realizarán consultas más adelante.
// 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>
);
}
Define los handlers por story a través de parameters.msw.handlers. En MSW v2, http.get reemplaza a rest.get, y la clase HttpResponse reemplaza a las utilidades de 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' },
]),
),
],
},
},
};
Modelando cada estado
El camino feliz es donde la mayoría de los tutoriales se detienen, y es la story menos interesante. El valor del mock en la capa de red radica en que un simple cambio de handler produce cada estado que tu componente puede alcanzar. Las reproducciones de sesión de interfaces de usuario basadas en API revelan con frecuencia estados que los desarrolladores nunca diseñaron en Storybook: un spinner que nunca se resuelve porque una solicitud quedó colgada, o una respuesta vacía que se renderiza como un layout roto en lugar de un estado vacío. Storybook junto con MSW es donde defines y verificas exactamente esos estados antes de que lleguen a producción.
| Estado | Handler | Qué demuestra |
|---|---|---|
| Carga | await delay('infinite') antes de responder | La interfaz pendiente se renderiza y no parpadea |
| Error | HttpResponse.json(null, { status: 500 }) | La rama de error gestiona un 5xx |
| Vacío | HttpResponse.json([]) | El layout sin resultados está diseñado, no roto |
La función delay acepta un modo 'infinite' que mantiene la solicitud pendiente indefinidamente, la forma más fiable de congelar un componente en su rama de carga. Importa delay desde msw y usa await dentro de un resolver asíncrono:
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 simula GraphQL de la misma manera (graphql.query('AllUsers', () => HttpResponse.json({ data }))), por lo que el patrón se aplica a Apollo, urql y React Query sin ningún cambio.
De la visualización a las pruebas
Una story con mock que solo se observa es documentación; añade una función play y se convierte en una prueba de componente automatizable. Importa expect desde storybook/test (el módulo actual, que reemplazó a @storybook/test de Storybook 8) y verifica el resultado renderizado:
import { expect } from 'storybook/test';
// en Success:
play: async ({ canvas }) => {
await expect(await canvas.findByText('Ada Lovelace')).toBeInTheDocument();
},
// en Loading:
play: async ({ canvas }) => {
await expect(canvas.getByRole('status')).toBeInTheDocument();
},
Para la story Loading con delay infinito, verifica que el spinner esté presente. No esperes la resolución, ya que la solicitud queda pendiente indefinidamente por diseño. Estas stories se ejecutan a través del addon de Vitest, que las ejecuta como pruebas de componente en el navegador Chromium de Playwright desde la interfaz de Storybook, la terminal o CI. Dado que los handlers de MSW son independientes del entorno, los mismos handlers de éxito, error y vacío respaldan una prueba independiente de Vitest mediante setupServer, y Chromatic captura instantáneas de cada story para regresión visual.
Realiza los mocks en la capa de red, modela los cuatro estados y adjunta una función play a cada uno: esto convierte una carpeta de stories en una suite de pruebas activa que detecta los bugs de carga infinita y estado vacío roto antes de que lleguen a producción. Comienza añadiendo las stories de carga, error y vacío a un componente de aplicación existente hoy mismo. Los handlers que escribas allí son los mismos que reutilizarán tus pruebas de Vitest.
Preguntas Frecuentes
¿Por qué mi componente se queda en 'Loading…' en Storybook incluso después de instalar msw-storybook-addon?
El componente está bloqueado porque o bien ningún handler de MSW coincide con su solicitud, o el loader del addon no está conectado. Confirma que añadiste mswLoader al array loaders en .storybook/preview y que llamaste a initialize(), que el directorio public fue generado con npx msw init y está listado en staticDirs, y que un handler en parameters.msw.handlers coincide con la URL y el método exactos de la solicitud. Una discrepancia en la URL deja la solicitud sin gestionar y el componente en espera indefinidamente.
¿Cuál es la diferencia entre hacer mocks en la capa de red con MSW y sustituir el hook de fetch?
El mock en la capa de red con MSW intercepta la solicitud saliente real y devuelve una respuesta, de modo que el componente ejecuta su código de obtención de datos real sin cambios y los mismos handlers funcionan en el navegador, en Node mediante setupServer y en Chromatic. Sustituir el hook reemplaza useQuery o fetch con datos predefinidos, lo que acopla la story a una biblioteca de datos concreta y a la estructura interna del componente, y no puede reutilizarse en una prueba de Node.
¿Funciona msw-storybook-addon con handlers de MSW v1 como rest.get y res(ctx.json())?
No. Desde la versión 2.0.0, el addon requiere MSW 2.0.0 o superior, y MSW v2 eliminó el namespace rest y la firma de resolver res(ctx.json()). Reescribe los handlers usando http.get y la clase HttpResponse, por ejemplo http.get(url, () => HttpResponse.json(data)). El código de MSW v1 no funcionará con el addon actual y debe migrarse siguiendo la guía oficial de migración de MSW de 1.x a 2.x.
¿Por qué mi story de carga con delay infinito se bloquea al ejecutarse como prueba?
Una story que usa await delay('infinite') mantiene su solicitud pendiente indefinidamente por diseño, por lo que una función play que espera la resolución de la interfaz nunca termina. Verifica únicamente que la interfaz pendiente esté presente, por ejemplo que el spinner con role status se renderice, en lugar de esperar la resolución. Si una prueba de Node o Vitest debe completarse correctamente, usa un delay finito como delay(1000) en lugar del modo infinito para ese escenario.
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