Simulación en tests
Simula módulos, funciones, APIs e internals de Next.js para aislar el código bajo prueba.
Busca en todas las páginas de la documentación
Simula módulos, funciones, APIs e internals de Next.js para aislar el código bajo prueba.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
next/navigationfetch con vi.stubGlobalmockPush para la aserciónvi.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 como fetch y lo restaura cuando llamas a vi.restoreAllMocks()vi.resetModules() entre testsMSW (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();
});// 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);vi.mock no se eleva correctamente - Vitest eleva vi.mock() al inicio del archivo, pero las variables definidas antes no están disponibles dentro de la factory. Solución: Usa vi.hoisted() para declarar variables que necesiten estar disponibles dentro de vi.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() o vi.restoreAllMocks() en beforeEach.
vi.mocked en funciones no mockeadas - vi.mocked(fetch) falla en tiempo de ejecución si fetch no ha sido reemplazado con vi.fn(). Solución: Siempre usa vi.stubGlobal("fetch", vi.fn()) antes de usar vi.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.
| 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 |
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.const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush, back: vi.fn() }),
usePathname: () => "/dashboard",
useSearchParams: () => new URLSearchParams(),
}));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 }),
}));vi.stubGlobal("fetch") para mocking rápido y puntual.Llama a vi.clearAllMocks() o vi.restoreAllMocks() en beforeEach. Esto reinicia el historial de llamadas y los valores de retorno entre tests.
vi.mock("next/image", () => ({
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => (
<img {...props} />
),
}));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á.
const mockFn = vi.fn<[string, number], boolean>();
vi.mock("@/lib/db", () => ({
getUser: vi.fn<[number], Promise<User>>(),
}));src/mocks/handlers.ts usando http.get(), http.post(), etc.src/mocks/server.ts con setupServer(...handlers).vitest.setup.ts, llama a server.listen() en beforeAll, server.resetHandlers() en afterEach y server.close() en afterAll.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.
const spy = vi.spyOn(analytics, "trackEvent");
render(<Dashboard />);
expect(spy).toHaveBeenCalledWith("page_view", { page: "dashboard" });
spy.mockRestore();jest.mock() funciona de la misma manera. Reemplaza vi.fn() con jest.fn() y vi.mocked() con jest.mocked(). Los patrones son idénticos.
Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥