Search across all documentation pages
Mock modules, functions, APIs, and Next.js internals to isolate the code under test.
Quick-reference recipe card -- copy-paste ready.
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} />
),
}));When to reach for this: When your component depends on external modules, API calls, or Next.js internals that you need to control in 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;
// 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
What this demonstrates:
next/navigationfetch with vi.stubGlobalmockPush for assertionvi.mock() replaces an entire module at the top of the file -- it is hoisted above imports by Vitest's transformvi.fn() creates a mock function that tracks calls, arguments, and return valuesvi.mocked() is a type helper that casts a function to its mocked type, enabling auto-completion for mock methodsvi.stubGlobal() replaces a global like fetch and restores it when you call vi.restoreAllMocks()vi.resetModules() between testsMSW (Mock Service Worker) for API mocking:
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"
// 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.// 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", ()
Mocking with Jest (same patterns, different API):
// jest.mock equivalent
jest.mock("next/navigation", () => ({
useRouter: () => ({ push: jest.fn() }),
usePathname: () => "/dashboard",
useSearchParams: () => new URLSearchParams(),
}));
// jest.fn equivalent
const
Spy on a method without fully mocking:
import * as analytics from "@/lib/analytics";
it("tracks page view", () => {
const spy = vi.spyOn(analytics, "trackEvent");
render(<Dashboard />);
expect(spy).toHaveBeenCalledWith("page_view"
// 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],
vi.mock not hoisted correctly -- Vitest hoists vi.mock() to the top of the file, but variables defined before it are not available inside the factory. Fix: Use vi.hoisted() to declare variables that need to be available inside vi.mock():
const \{ mockPush \} = vi.hoisted(() => (\{
mockPush: vi.fn(),
\}));
vi.mock("next/navigation", () => (\{
useRouter: () => (\{ push: mockPush \}),
| Alternative | Use When | Don't Use When |
|---|---|---|
| MSW | You want network-level API mocking that works in tests and Storybook | You need to mock non-HTTP modules |
vi.mock / jest.mock | You need to replace module internals (hooks, utility functions) | You only need to mock HTTP calls (prefer MSW) |
vi.spyOn / jest.spyOn | You want to observe calls without changing behavior | You need to completely replace a module |
| Dependency injection | Architecture supports passing dependencies as props or config | You would need to refactor heavily |
vi.mock(), vi.fn(), and vi.spyOn()?vi.mock() replaces an entire module at the top of the file.vi.fn() creates a standalone mock function that tracks calls.vi.spyOn() wraps an existing method to observe calls without fully replacing the module.next/navigation in a test?const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush, back: vi.fn() }),
usePathname: () => "/dashboard",
useSearchParams: () => new URLSearchParams
vi.mock() factory?vi.mock() is hoisted above imports, so variables declared before it are not available inside the factory. Use vi.hoisted():
const { mockPush } = vi.hoisted(() => ({
mockPush: vi.fn(),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush }),
}));vi.stubGlobal("fetch")?vi.stubGlobal("fetch") for quick one-off mocking.Call vi.clearAllMocks() or vi.restoreAllMocks() in beforeEach. This resets call history and return values between tests.
next/image in tests?vi.mock("next/image", () => ({
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => (
<img {...props} />
),
}));vi.mocked(fetch) throw at runtime?vi.mocked() is a type helper only. If fetch has not been replaced with vi.fn() via vi.stubGlobal("fetch", vi.fn()), calling mock methods on it will fail.
const mockFn = vi.fn<[string, number], boolean>();
vi.mock("@/lib/db", () => ({
getUser: vi.fn<[number], Promise<User>>(),
}));src/mocks/handlers.ts using http.get(), http.post(), etc.src/mocks/server.ts with setupServer(...handlers).vitest.setup.ts, call server.listen() in beforeAll, server.resetHandlers() in afterEach, and server.close() in afterAll.onUnhandledRequest: "error" do in MSW?It throws an error for any fetch request that does not match a defined handler. This ensures all API calls in your tests are explicitly handled. Use "warn" during development if this is too strict.
const spy = vi.spyOn(analytics, "trackEvent");
render(<Dashboard />);
expect(spy).toHaveBeenCalledWith("page_view", { page: "dashboard" });
spy.mockRestore();vi.mock()?jest.mock() works the same way. Replace vi.fn() with jest.fn() and vi.mocked() with jest.mocked(). The patterns are identical.
Mocking too much -- Mocking every dependency makes tests pass even when integrations are broken. Fix: Use MSW for API mocking (tests real fetch code) and only mock what is truly external.
Mock state leaking between tests -- Mock return values persist across tests in the same file. Fix: Call vi.clearAllMocks() or vi.restoreAllMocks() in beforeEach.
vi.mocked on non-mocked functions -- vi.mocked(fetch) fails at runtime if fetch has not been replaced with vi.fn(). Fix: Always vi.stubGlobal("fetch", vi.fn()) before using vi.mocked(fetch).
MSW intercepting all requests -- onUnhandledRequest: "error" throws for unmatched requests. Fix: Provide handlers for all requests your tests make, or use "warn" during development.