Ir al contenido
angel.library

MSW — simular APIs en navegador y Node

MSW (Mock Service Worker) intercepta solicitudes HTTP con handlers declarativos. En navegador utiliza un Service Worker; en Node intercepta la red del proceso. El código de aplicación continúa usando fetch, Axios u otro cliente real, por lo que la prueba conserva URL, método, headers, serialización y manejo de respuesta.

Ventana de terminal
pnpm add -D msw
NecesidadMSWAlternativa
componente consume una APIsímockear hook oculta HTTP
forzar 500, 429 o demorasíproveedor real no es determinista
probar una query SQLnobase real/Testcontainers
confirmar contrato externo realparcialmentesandbox o contract test
probar backend propio en E2Enormalmente noentorno desplegado real

MSW simula la respuesta, no demuestra que el servidor real la produzca. Combínalo con contratos e integración del backend.

test/mocks/handlers.ts
import { HttpResponse, http } from "msw"
export const handlers = [
http.get("/api/users/:id", ({ params }) => {
return HttpResponse.json({
id: params.id,
displayName: "Ana"
})
})
]

Un handler describe el protocolo. Mantén respuestas pequeñas y válidas; crea overrides por prueba para el caso que cambia.

test/mocks/server.ts
import { setupServer } from "msw/node"
import { handlers } from "./handlers"
export const server = setupServer(...handlers)
test/setup.ts
import { afterAll, afterEach, beforeAll } from "vitest"
import { server } from "./mocks/server"
beforeAll(() => server.listen({ onUnhandledRequest: "error" }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

onUnhandledRequest: 'error' evita que una prueba haga red real por accidente. resetHandlers elimina overrides temporales y conserva los handlers iniciales.

it("muestra el perfil recibido", async () => {
render(<UserProfile userId="u_1" />)
expect(await screen.findByRole("heading", { name: "Ana" })).toBeVisible()
})
it("permite reintentar después de un error", async () => {
server.use(
http.get("/api/users/:id", () => {
return HttpResponse.json({ code: "TEMPORARY_FAILURE" }, { status: 503 })
})
)
const user = userEvent.setup()
render(<UserProfile userId="u_1" />)
expect(await screen.findByRole("alert")).toHaveTextContent(
/intenta de nuevo/i
)
await user.click(screen.getByRole("button", { name: /reintentar/i }))
})

El caso todavía necesita decidir qué respuesta tendrá el segundo intento. Puedes usar un contador local en el handler o reemplazarlo antes del clic; mantén esa secuencia explícita.

http.post("/api/orders", async ({ request }) => {
const body = (await request.json()) as { productId?: string }
const authorization = request.headers.get("authorization")
if (!authorization) {
return HttpResponse.json({ code: "UNAUTHORIZED" }, { status: 401 })
}
if (!body.productId) {
return HttpResponse.json({ code: "INVALID_BODY" }, { status: 400 })
}
return HttpResponse.json({ id: "order_1" }, { status: 201 })
})

No dupliques todo el backend dentro del handler. Comprueba únicamente aspectos necesarios para que un request incorrecto no reciba éxito automáticamente.

import { HttpResponse, delay, http } from "msw"
server.use(
http.get("/api/search", async () => {
await delay(2000)
return HttpResponse.json({ results: [] })
})
)

Prueba carga, cancelación, timeout, JSON inválido y status inesperados. Evita demoras reales largas en toda la suite; usa timers o una demora corta cuando solo importa el estado de carga.

Los mismos handlers pueden alimentar desarrollo local, historias y pruebas. En navegador se genera un worker público y se inicia antes de la aplicación. No habilites mocks accidentalmente en producción; condiciona el import al entorno y comprueba el bundle.

  • permitir request no manejado y llamar Internet desde CI;
  • handler global que filtra estado entre tests;
  • respuesta que no coincide con el contrato real;
  • recrear lógica completa del servidor;
  • interceptar el backend que un E2E pretende verificar;
  • usar datos sensibles copiados de producción.