Simulación en tests
Simula módulos, funciones, APIs e internals de Next.js para aislar el código bajo prueba.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
import { vi } from "vitest";
// Mock a module
vi.mock("@/lib/analytics", () => ({
trackEvent: vi.fn(),
}));
// Mock a function
const onSubmit = vi.fn();
onSubmit.mockResolvedValueOnce({ success: true });
// Mock fetch
vi.stubGlobal("fetch", vi.fn());
vi.mocked(fetch).mockResolvedValue(
new Response(JSON.stringify({ data: [] }), { status: 200 })
);
// Mock next/navigation
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), back: vi.fn(), refresh: vi.fn() }),
usePathname: () => "/dashboard",
useSearchParams: () => new URLSearchParams("?tab=settings"),
}));
// Mock next/image
vi.mock("next/image", () => ({
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => (
<img {...props} />
),
}));Cuándo usarlo: Cuando tu componente depende de módulos externos, llamadas a API o internals de Next.js que necesitas controlar en los tests.
Ejemplo funcional
// src/components/user-profile.tsx
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
interface User {
id: number;
name: string;
email: string;
}
export function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
useEffect(() => {
async function loadUser() {
try {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error("Failed to load user");
setUser(await res.json());
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
}
loadUser();
}, [userId]);
if (loading) return <p>Loading...</p>;
if (error) return <p role="alert">{error}</p>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={() => router.push(`/users/${userId}/edit`)}>
Edit Profile
</button>
</div>
);
}// src/components/user-profile.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, describe, it, expect, beforeEach } from "vitest";
import { UserProfile } from "./user-profile";
// Mock next/navigation
const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: mockPush,
back: vi.fn(),
refresh: vi.fn(),
}),
usePathname: () => "/users/1",
useSearchParams: () => new URLSearchParams(),
}));
describe("UserProfile", () => {
const user = userEvent.setup();
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn());
mockPush.mockClear();
});
it("shows loading state initially", () => {
vi.mocked(fetch).mockResolvedValue(
new Response(JSON.stringify({ id: 1, name: "Alice", email: "alice@test.com" }))
);
render(<UserProfile userId={1} />);
expect(screen.getByText("Loading...")).toBeInTheDocument();
});
it("renders user data on success", async () => {
vi.mocked(fetch).mockResolvedValue(
new Response(JSON.stringify({ id: 1, name: "Alice", email: "alice@test.com" }))
);
render(<UserProfile userId={1} />);
expect(await screen.findByText("Alice")).toBeInTheDocument();
expect(screen.getByText("alice@test.com")).toBeInTheDocument();
});
it("shows error on fetch failure", async () => {
vi.mocked(fetch).mockResolvedValue(new Response(null, { status: 500 }));
render(<UserProfile userId={1} />);
expect(await screen.findByRole("alert")).toHaveTextContent(
"Failed to load user"
);
});
it("navigates to edit page on button click", async () => {
vi.mocked(fetch).mockResolvedValue(
new Response(JSON.stringify({ id: 1, name: "Alice", email: "alice@test.com" }))
);
render(<UserProfile userId={1} />);
await screen.findByText("Alice");
await user.click(screen.getByRole("button", { name: /edit profile/i }));
expect(mockPush).toHaveBeenCalledWith("/users/1/edit");
});
});Lo que demuestra esto:
- Mock a nivel de módulo para
next/navigation - Simular
fetchconvi.stubGlobal - Extraer
mockPushpara la aserción - Probar estados de carga, éxito, error y navegación
Profundización
Cómo funciona
vi.mock()reemplaza un módulo completo al inicio del archivo — Vitest lo eleva (hoist) por encima de los imports mediante su transformaciónvi.fn()crea una función mock que rastrea llamadas, argumentos y valores de retornovi.mocked()es un helper de tipos que convierte una función a su tipo mockeado, habilitando autocompletado para métodos de mockvi.stubGlobal()reemplaza un global comofetchy lo restaura cuando llamas avi.restoreAllMocks()- Los mocks de módulo persisten durante todo el archivo de test a menos que llames a
vi.resetModules()entre tests
Variaciones
MSW (Mock Service Worker) para mocking de API:
npm install -D msw// src/mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/users/:id", ({ params }) => {
return HttpResponse.json({
id: Number(params.id),
name: "Alice",
email: "alice@test.com",
});
}),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 99, ...body }, { status: 201 });
}),
http.get("/api/users/:id", () => {
return HttpResponse.json(
{ message: "Not found" },
{ status: 404 }
);
}),
];// src/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);// vitest.setup.ts
import { server } from "./src/mocks/server";
import { beforeAll, afterAll, afterEach } from "vitest";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());// Test using MSW -- no manual fetch mock needed
import { http, HttpResponse } from "msw";
import { server } from "@/mocks/server";
it("handles 404", async () => {
// Override for this specific test
server.use(
http.get("/api/users/:id", () => {
return HttpResponse.json({ message: "Not found" }, { status: 404 });
})
);
render(<UserProfile userId={999} />);
expect(await screen.findByRole("alert")).toBeInTheDocument();
});Mocking con Jest (mismos patrones, API distinta):
// jest.mock equivalent
jest.mock("next/navigation", () => ({
useRouter: () => ({ push: jest.fn() }),
usePathname: () => "/dashboard",
useSearchParams: () => new URLSearchParams(),
}));
// jest.fn equivalent
const handleSubmit = jest.fn();
handleSubmit.mockResolvedValue({ success: true });Espiar un método sin simular por completo:
import * as analytics from "@/lib/analytics";
it("tracks page view", () => {
const spy = vi.spyOn(analytics, "trackEvent");
render(<Dashboard />);
expect(spy).toHaveBeenCalledWith("page_view", { page: "dashboard" });
spy.mockRestore();
});Notas de TypeScript
// vi.mocked provides full type safety
vi.mocked(fetch).mockResolvedValue(
new Response(JSON.stringify(data)) // TypeScript knows fetch signature
);
// Typing mock implementations
vi.mock("@/lib/db", () => ({
getUser: vi.fn<[number], Promise<User>>(),
}));
// Asserting mock calls with types
const mockFn = vi.fn<[string, number], boolean>();
mockFn("hello", 42);
expect(mockFn).toHaveBeenCalledWith("hello", 42);Errores comunes
-
vi.mockno se eleva correctamente — Vitest elevavi.mock()al inicio del archivo, pero las variables definidas antes no están disponibles dentro de la factory. Solución: Usavi.hoisted()para declarar variables que necesiten estar disponibles dentro devi.mock():const \{ mockPush \} = vi.hoisted(() => (\{ mockPush: vi.fn(), \})); vi.mock("next/navigation", () => (\{ useRouter: () => (\{ push: mockPush \}), \})); -
Simular demasiado — Simular cada dependencia hace que los tests pasen incluso cuando las integraciones están rotas. Solución: Usa MSW para mocking de API (prueba el código real de fetch) y solo simula lo que es verdaderamente externo.
-
El estado del mock se filtra entre tests — Los valores de retorno del mock persisten entre tests del mismo archivo. Solución: Llama a
vi.clearAllMocks()ovi.restoreAllMocks()enbeforeEach. -
vi.mockeden funciones no mockeadas —vi.mocked(fetch)falla en tiempo de ejecución sifetchno ha sido reemplazado convi.fn(). Solución: Siempre usavi.stubGlobal("fetch", vi.fn())antes de usarvi.mocked(fetch). -
MSW intercepta todas las solicitudes —
onUnhandledRequest: "error"lanza una excepción para solicitudes sin coincidencia. Solución: Proporciona handlers para todas las solicitudes que hagan tus tests, o usa"warn"durante el desarrollo.
Alternativas
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
| MSW | Quieres mocking de API a nivel de red que funciona en tests y Storybook | Necesitas simular módulos que no son HTTP |
vi.mock / jest.mock | Necesitas reemplazar internals de módulos (hooks, funciones de utilidad) | Solo necesitas simular llamadas HTTP (prefiere MSW) |
vi.spyOn / jest.spyOn | Quieres observar llamadas sin cambiar el comportamiento | Necesitas reemplazar un módulo por completo |
| Inyección de dependencias | La arquitectura admite pasar dependencias como props o config | Necesitarías refactorizar mucho |
FAQs
¿Cuál es la diferencia entre vi.mock(), vi.fn() y vi.spyOn()?
vi.mock()reemplaza un módulo completo al inicio del archivo.vi.fn()crea una función mock independiente que rastrea llamadas.vi.spyOn()envuelve un método existente para observar llamadas sin reemplazar el módulo por completo.
¿Cómo simulo next/navigation en un test?
const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush, back: vi.fn() }),
usePathname: () => "/dashboard",
useSearchParams: () => new URLSearchParams(),
}));Trampa: ¿Por qué no puedo referenciar variables dentro de una factory de vi.mock()?
vi.mock() se eleva por encima de los imports, así que las variables declaradas antes no están disponibles dentro de la factory. Usa vi.hoisted():
const { mockPush } = vi.hoisted(() => ({
mockPush: vi.fn(),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush }),
}));¿Qué es MSW y cuándo debería usarlo en lugar de vi.stubGlobal("fetch")?
- MSW (Mock Service Worker) intercepta fetch a nivel de red.
- Usa MSW cuando tienes muchos endpoints de API o quieres mocks que funcionen tanto en tests como en Storybook.
- Usa
vi.stubGlobal("fetch")para mocking rápido y puntual.
¿Cómo evito que el estado del mock se filtre entre tests?
Llama a vi.clearAllMocks() o vi.restoreAllMocks() en beforeEach. Esto reinicia el historial de llamadas y los valores de retorno entre tests.
¿Cómo simulo next/image en tests?
vi.mock("next/image", () => ({
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => (
<img {...props} />
),
}));Trampa: ¿Por qué vi.mocked(fetch) lanza una excepción en tiempo de ejecución?
vi.mocked() es solo un helper de tipos. Si fetch no ha sido reemplazado con vi.fn() mediante vi.stubGlobal("fetch", vi.fn()), llamar a métodos de mock sobre él fallará.
¿Cómo tipar parámetros y valores de retorno de funciones mock en TypeScript?
const mockFn = vi.fn<[string, number], boolean>();
vi.mock("@/lib/db", () => ({
getUser: vi.fn<[number], Promise<User>>(),
}));¿Cómo configuro MSW con Vitest?
- Crea handlers en
src/mocks/handlers.tsusandohttp.get(),http.post(), etc. - Crea un servidor en
src/mocks/server.tsconsetupServer(...handlers). - En
vitest.setup.ts, llama aserver.listen()enbeforeAll,server.resetHandlers()enafterEachyserver.close()enafterAll.
¿Qué hace onUnhandledRequest: "error" en MSW?
Lanza un error para cualquier solicitud fetch que no coincida con un handler definido. Esto asegura que todas las llamadas a API en tus tests estén explícitamente manejadas. Usa "warn" durante el desarrollo si esto es demasiado estricto.
¿Cómo espío una función sin cambiar su comportamiento?
const spy = vi.spyOn(analytics, "trackEvent");
render(<Dashboard />);
expect(spy).toHaveBeenCalledWith("page_view", { page: "dashboard" });
spy.mockRestore();¿Cuál es el equivalente de Jest para vi.mock()?
jest.mock() funciona de la misma manera. Reemplaza vi.fn() con jest.fn() y vi.mocked() con jest.mocked(). Los patrones son idénticos.
Relacionado
- Probar comportamiento asíncrono — usar MSW con tests asíncronos
- Probar formularios — simular manejadores de envío de formularios
- Probar Server Components y Actions — simular base de datos y servicios externos
- Fundamentos de React Testing Library — patrones básicos de testing