En el capítulo anterior simulabas una dependencia inyectándola: el componente recibía
cargarHeroes por props y tú le pasabas un doble. Limpio cuando puedes diseñarlo así. Pero muchas
veces el componente hace su propio fetch por dentro, sin punto de inyección. Y aunque lo
tuviera, a veces quieres probar la petición real: que va a la URL correcta, que maneja un 500.
Para eso está MSW (Mock Service Worker).
Interceptar la red, no parchear fetch#
La idea de MSW es distinta a todo lo anterior. No sustituyes una función ni parcheas fetch:
interceptas la petición a nivel de red. El componente hace su fetch("...") real, como en
producción, y MSW lo atrapa y responde lo que tú digas. El componente no se entera ni cambia.
Las piezas:
- Un handler describe cómo responder a una petición:
http.get(url, resolver), donde el resolver devuelve una respuesta conHttpResponse.json(datos). Es tu API de mentira, declarada. setupServer(...handlers)reúne los handlers en un servidor para los tests.- El ciclo de vida:
server.listen()enciende la intercepción (enbeforeAll),server.close()la apaga (enafterAll), yserver.resetHandlers()(enafterEach) limpia entre tests.
Probar el error es la mitad del trabajo#
Fíjate en el segundo test del demo. Probar que el caso feliz funciona está bien, pero el código que
pide datos falla de verdad: la API devuelve un 500, la red se cae, llega basura. Con MSW, simular
eso es trivial: server.use sobrescribe un handler solo para ese test.
// Solo en este test: la API responde 500. El componente debe mostrar su estado de error.
server.use(
http.get("https://api.equipo.test/miembros", () => new HttpResponse(null, { status: 500 })),
);¿Y por qué importa tanto? Porque el camino de error es donde más bugs viven y donde menos se
prueba (es incómodo provocar un fallo real). Un componente que pinta bien los datos pero se queda en
blanco —o crashea— cuando la API falla es un bug que tu usuario sí verá. Con MSW lo pruebas en una
línea, y afterEach(resetHandlers) se encarga de que ese 500 no se cuele en el siguiente test.
Por qué MSW es el estándar de 2026#
Podrías parchear fetch a mano (vi.spyOn(globalThis, "fetch")). Funciona, pero te acopla a la
API exacta de fetch: si mañana el código usa axios, o cambian los argumentos, tu test se rompe
aunque el comportamiento siga bien. MSW intercepta por debajo de la librería que uses: tus tests
no se atan a cómo se hace la llamada, solo a la red.
Y hay un bonus que ninguna otra opción da: los mismos handlers sirven en los tests y en el navegador mientras desarrollas (MSW puede simular el backend en local con un service worker). Una descripción de tu API, dos usos: testear y desarrollar sin backend. Por eso es el estándar.
En este playground,
setupServercorre sobre un interceptor defetchadaptado al navegador, pero el código que escribes es el mismo que usarías en tu proyecto con Vitest. En el navegador real, MSW usa un service worker (de ahí su nombre); en los tests de Node, elsetupServerque ves.
Pruébalo#
Abre el demo de arriba y cambia la respuesta del handler feliz: pon HttpResponse.json([]) (una lista
vacía) y mira cómo el primer test pasa a rojo —ya no encuentra “Genji”—. Devuélvelo a su sitio. Has
cambiado lo que “responde la API” sin tocar el componente: esa es la idea entera de MSW.
Comprueba lo que sabes#
Pregunta 1 de 5
En el capítulo anterior inyectabas la dependencia (cargarHeroes por props). ¿Qué hace MSW distinto?
Tu turno#
Prueba PanelHeroes con MSW: monta el servidor, gestiona su ciclo de vida y cubre el caso feliz y el
de error. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un tier
al siguiente: del caso feliz a la carga y el error, y de ahí al contrato completo.
Ejercicio · en esta página
Prueba PanelHeroes con MSW
Tienes PanelHeroes, un componente que hace su PROPIO fetch para cargar héroes (carga, datos, error, lista vacía). No tocas el componente: pruébalo con MSW. Monta un servidor con un handler, gestiona su ciclo de vida (listen/resetHandlers/close) y cubre el caso feliz y el de error con server.use.
Paso 1: El caso feliz interceptado
- Montas un servidor MSW con un handler que responde a la petición del panel.
- Gestionas su ciclo de vida con beforeAll(listen), afterEach(resetHandlers) y afterAll(close).
- Compruebas con findByText que el panel pinta los héroes que "devolvió la API".
Paso 2: La carga y el error
- Compruebas el estado de carga inicial (getByText, síncrono).
- Pruebas el caso de ERROR: con server.use, fuerzas un 500 solo en ese test y compruebas el aviso.
Paso 3: El contrato completo frente a la red
- Cubres datos, carga, error (500) y lista vacía, cada uno con su handler.
- Usas server.use para los casos que se desvían del feliz, confiando en afterEach(resetHandlers) para aislarlos.
- Queda claro, leyendo los tests, cómo se comporta el panel ante cada respuesta de la API.
Ver soluciones
// Tier OK: el caso feliz. Un handler de MSW responde y el panel pinta los héroes.
import { describe, it, expect, beforeAll, afterEach, afterAll } from "vitest";
import { render, screen } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { PanelHeroes } from "./PanelHeroes";
const server = setupServer(
http.get("https://api.equipo.test/heroes", () =>
HttpResponse.json([
{ nombre: "Genji", rol: "Daño" },
{ nombre: "Mercy", rol: "Apoyo" },
]),
),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe("PanelHeroes", () => {
it("muestra los héroes que vienen de la API", async () => {
// El componente hace su fetch REAL; MSW lo intercepta y responde el handler de arriba.
render(<PanelHeroes />);
// findByText espera a que la respuesta llegue y el panel la pinte.
expect(await screen.findByText("Genji")).toBeInTheDocument();
expect(screen.getByText("Mercy")).toBeInTheDocument();
});
}); Por qué este nivel
- Monta un servidor MSW, gestiona su ciclo de vida (listen/resetHandlers/close) y prueba el caso feliz con findByText. El componente hace su fetch REAL: MSW intercepta la red, no parchea nada del componente.
- Su límite: solo el caso feliz. Lo más valioso de testear la red —el estado de carga y, sobre todo, qué pasa cuando la API FALLA— se queda fuera.
// Tier mejor: el caso feliz, el estado de carga y —lo importante con red— el caso de ERROR,
// sobrescribiendo el handler solo en ese test con server.use.
import { describe, it, expect, beforeAll, afterEach, afterAll } from "vitest";
import { render, screen } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { PanelHeroes } from "./PanelHeroes";
const server = setupServer(
http.get("https://api.equipo.test/heroes", () =>
HttpResponse.json([
{ nombre: "Genji", rol: "Daño" },
{ nombre: "Mercy", rol: "Apoyo" },
]),
),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe("PanelHeroes", () => {
it("muestra los héroes que vienen de la API", async () => {
render(<PanelHeroes />);
expect(await screen.findByText("Genji")).toBeInTheDocument();
});
it("muestra 'Cargando…' mientras llega la respuesta", () => {
// Justo tras render, la petición está en marcha: el cargando se ve (síncrono).
render(<PanelHeroes />);
expect(screen.getByText("Cargando héroes…")).toBeInTheDocument();
});
it("avisa cuando la API falla", async () => {
// server.use SOLO para este test: sobrescribe el handler para devolver un 500.
server.use(
http.get(
"https://api.equipo.test/heroes",
() => new HttpResponse(null, { status: 500 }),
),
);
render(<PanelHeroes />);
// El panel cae al estado de error. afterEach(resetHandlers) deshará este override.
expect(
await screen.findByText("No se pudieron cargar los héroes."),
).toBeInTheDocument();
});
}); Por qué es mejor que el anterior
- Añade el estado de carga y, sobre todo, el caso de ERROR: con server.use sobrescribe el handler para devolver un 500 solo en ese test. Probar el fallo es la mitad del valor de testear código que pide datos.
- afterEach(resetHandlers) deshace el override del 500: los demás tests vuelven a ver el handler feliz. Sin ese reset, el 500 contaminaría a los siguientes.
// Tier excelente: el contrato completo del panel frente a la red real —datos, carga, error y
// lista vacía— con MSW interceptando, sin tocar el componente y de forma determinista.
import { describe, it, expect, beforeAll, afterEach, afterAll } from "vitest";
import { render, screen } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { PanelHeroes } from "./PanelHeroes";
const URL_HEROES = "https://api.equipo.test/heroes";
const server = setupServer(
http.get(URL_HEROES, () =>
HttpResponse.json([
{ nombre: "Genji", rol: "Daño" },
{ nombre: "Mercy", rol: "Apoyo" },
]),
),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe("PanelHeroes", () => {
it("muestra los héroes que devuelve la API", async () => {
render(<PanelHeroes />);
expect(await screen.findByText("Genji")).toBeInTheDocument();
expect(screen.getByText("Mercy")).toBeInTheDocument();
});
it("muestra el estado de carga antes de la respuesta", () => {
render(<PanelHeroes />);
expect(screen.getByText("Cargando héroes…")).toBeInTheDocument();
});
it("avisa cuando la API responde con error (500)", async () => {
server.use(
http.get(URL_HEROES, () => new HttpResponse(null, { status: 500 })),
);
render(<PanelHeroes />);
expect(
await screen.findByText("No se pudieron cargar los héroes."),
).toBeInTheDocument();
});
it("avisa cuando la API responde con una lista vacía", async () => {
server.use(http.get(URL_HEROES, () => HttpResponse.json([])));
render(<PanelHeroes />);
expect(await screen.findByText("No hay héroes.")).toBeInTheDocument();
});
}); Por qué es mejor que el anterior
- Cubre el contrato completo frente a la red: datos, carga, error (500) y lista vacía, cada uno con su handler. Esos cuatro estados son los que de verdad ocurren al pedir datos.
- Cada caso que se desvía del feliz usa server.use y confía en afterEach(resetHandlers) para no contaminar al resto. Leyendo los tests se entiende cómo se comporta el panel ante cualquier respuesta de la API, sin haber tocado el componente.