Step 2
vitest basics + mock patterns
30 min
vitest basics + mock patterns
The Jest successor. ESM-first, Vite-native, fast.
1. Install
pnpm add -D vitest @vitest/ui
{ "scripts": { "test": "vitest run", "test:watch": "vitest" } }
2. First test
// src/lib/format.test.ts
import { describe, it, expect } from "vitest";
import { formatDate } from "./format";
describe("formatDate", () => {
it("YYYY-MM-DD", () => {
expect(formatDate(new Date("2026-05-06"))).toBe("2026-05-06");
});
});
3. Environment — node vs jsdom
export default defineConfig({
test: { environment: "node", globals: true },
});
Use jsdom for DOM-dependent code. Per-file override:
// @vitest-environment jsdom
4. vi.hoisted
const { mockQuery } = vi.hoisted(() => ({ mockQuery: vi.fn() }));
vi.mock("@/shared/lib/db", () => ({ pool: { query: mockQuery } }));
import { getUserById } from "./user-repo";
it("returns row", async () => {
mockQuery.mockResolvedValueOnce({ rows: [{ id: 1 }] });
expect(await getUserById(1)).toEqual({ id: 1 });
});
vi.mock hoists but forbids outer references. Hoist the mock construction with vi.hoisted.
5. env · global stubs
beforeEach(() => {
vi.stubEnv("REVALIDATE_URL", "http://localhost:3000");
vi.resetModules();
});
afterEach(() => { vi.unstubAllEnvs(); vi.unstubAllGlobals(); });
it("fetch called", async () => {
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 });
vi.stubGlobal("fetch", fetchMock);
await triggerRevalidate();
expect(fetchMock).toHaveBeenCalled();
});
6. React components
pnpm add -D @testing-library/react @testing-library/jest-dom
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
it("increments on click", async () => {
render(<Counter />);
await userEvent.click(screen.getByRole("button", { name: /increment/i }));
expect(screen.getByText("1")).toBeInTheDocument();
});
7. snapshots — carefully
expect(user).toMatchSnapshot();
Convenient but snapshots rot. Only for complex outputs.
8. Coverage
pnpm add -D @vitest/coverage-v8
vitest run --coverage
test: {
coverage: {
provider: "v8",
exclude: ["**/*.test.ts", "**/dist/**"],
thresholds: { lines: 70, functions: 70, branches: 70 },
},
}
9. Gotchas
- "Cannot access before initialization" on
vi.mockvars → usevi.hoisted - Missing
afterEachcleanup → env stubs leak into later tests - No
windowin tests → setenvironment: "jsdom" - Stale snapshots → review periodic regenerations
Closing
Nail vi.hoisted, stubEnv, and stubGlobal and you can handle virtually any mocking situation.
Next
- 03-pytest-fixtures