Vitest
"Vitest: unit/integration testing, Vite-native, mocking (vi.mock/vi.fn), snapshots, coverage, workspace, in-source testing, benchmark"
Vitest is a Vite-native testing framework that reuses your Vite configuration, plugins, and transformation pipeline for tests. This eliminates the double-configuration problem where your test runner and build tool disagree on how to handle TypeScript, JSX, CSS imports, or path aliases. Vitest provides Jest-compatible APIs so migration is straightforward, while adding Vite-specific features like hot module replacement for tests, in-source testing, and native ESM support. Tests run in isolated worker threads by default, ensuring one test file cannot leak state into another. The focus is on speed: Vitest only transforms and runs files that have changed, making the watch-mode feedback loop nearly instant. ## Key Points - Reuse your `vite.config.ts` by extending it in `vitest.config.ts` so path aliases, plugins, and transforms stay in sync. - Use `vi.clearAllMocks()` in `beforeEach` to prevent mock state from leaking between tests. - Prefer `vi.mocked()` for type-safe access to mock functions rather than casting manually. - Use `vi.useFakeTimers()` for anything time-dependent (debounce, throttle, setTimeout) and always call `vi.useRealTimers()` afterward. - Enable coverage thresholds in CI to prevent regression in test coverage. - Prefer inline snapshots (`toMatchInlineSnapshot`) for small values so the expected output is visible in the test file. - Use workspace configuration to run unit, component, and integration tests with different environments in a single command. - Do not import from `jest`; use `vitest` imports. The APIs are compatible but the runtime is different. - Avoid mocking everything; if a function is pure and fast, test it directly without mocks. - Do not use `vi.mock` inside `it` or `describe` blocks; it is hoisted to the top of the file regardless, which causes confusion. - Avoid `toMatchSnapshot()` for large objects that change frequently; inline snapshots or explicit assertions are more maintainable. - Do not rely on test execution order within a file; each test should set up its own preconditions.
skilldb get testing-services-skills/VitestFull skill: 370 linesVitest
Core Philosophy
Vitest is a Vite-native testing framework that reuses your Vite configuration, plugins, and transformation pipeline for tests. This eliminates the double-configuration problem where your test runner and build tool disagree on how to handle TypeScript, JSX, CSS imports, or path aliases. Vitest provides Jest-compatible APIs so migration is straightforward, while adding Vite-specific features like hot module replacement for tests, in-source testing, and native ESM support. Tests run in isolated worker threads by default, ensuring one test file cannot leak state into another. The focus is on speed: Vitest only transforms and runs files that have changed, making the watch-mode feedback loop nearly instant.
Setup
Installation and Configuration
// npm install -D vitest @vitest/coverage-v8 @vitest/ui
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html", "lcov"],
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.d.ts",
"src/**/*.test.*",
"src/**/index.ts",
"src/test/**",
],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
pool: "threads",
reporters: ["default", "junit"],
outputFile: { junit: "./test-results.xml" },
},
});
Setup File
// src/test/setup.ts
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach, vi } from "vitest";
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
// Mock browser APIs not available in jsdom
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
Key Techniques
Basic Unit Testing
// src/utils/format.ts
export function formatCurrency(cents: number, locale = "en-US"): string {
return new Intl.NumberFormat(locale, {
style: "currency",
currency: "USD",
}).format(cents / 100);
}
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.trim();
}
// src/utils/format.test.ts
import { describe, it, expect } from "vitest";
import { formatCurrency, slugify } from "./format";
describe("formatCurrency", () => {
it("converts cents to dollar string", () => {
expect(formatCurrency(1099)).toBe("$10.99");
expect(formatCurrency(0)).toBe("$0.00");
expect(formatCurrency(100000)).toBe("$1,000.00");
});
it("handles negative amounts", () => {
expect(formatCurrency(-500)).toBe("-$5.00");
});
});
describe("slugify", () => {
it.each([
["Hello World", "hello-world"],
[" Multiple Spaces ", "multiple-spaces"],
["Special Ch@r$!", "special-chr"],
["already-slugged", "already-slugged"],
])("converts '%s' to '%s'", (input, expected) => {
expect(slugify(input)).toBe(expected);
});
});
Mocking with vi.mock and vi.fn
// src/services/user.service.ts
import { db } from "../lib/database";
import { sendEmail } from "../lib/email";
export async function createUser(name: string, email: string) {
const existing = await db.user.findUnique({ where: { email } });
if (existing) throw new Error("Email already registered");
const user = await db.user.create({ data: { name, email } });
await sendEmail(email, "Welcome!", `Hello ${name}, welcome aboard!`);
return user;
}
// src/services/user.service.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createUser } from "./user.service";
// Module-level mock (hoisted automatically)
vi.mock("../lib/database", () => ({
db: {
user: {
findUnique: vi.fn(),
create: vi.fn(),
},
},
}));
vi.mock("../lib/email", () => ({
sendEmail: vi.fn().mockResolvedValue(undefined),
}));
import { db } from "../lib/database";
import { sendEmail } from "../lib/email";
const mockFindUnique = vi.mocked(db.user.findUnique);
const mockCreate = vi.mocked(db.user.create);
const mockSendEmail = vi.mocked(sendEmail);
describe("createUser", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("creates user and sends welcome email", async () => {
mockFindUnique.mockResolvedValue(null);
mockCreate.mockResolvedValue({ id: "1", name: "Alice", email: "a@b.com" });
const user = await createUser("Alice", "a@b.com");
expect(user.id).toBe("1");
expect(mockCreate).toHaveBeenCalledWith({
data: { name: "Alice", email: "a@b.com" },
});
expect(mockSendEmail).toHaveBeenCalledWith(
"a@b.com",
"Welcome!",
expect.stringContaining("Alice")
);
});
it("throws when email already exists", async () => {
mockFindUnique.mockResolvedValue({ id: "1", name: "X", email: "a@b.com" });
await expect(createUser("Alice", "a@b.com")).rejects.toThrow(
"Email already registered"
);
expect(mockCreate).not.toHaveBeenCalled();
expect(mockSendEmail).not.toHaveBeenCalled();
});
});
Spy and Partial Mocking
import { describe, it, expect, vi } from "vitest";
// Partial mock: only mock specific exports
vi.mock("../lib/analytics", async (importOriginal) => {
const actual = await importOriginal<typeof import("../lib/analytics")>();
return {
...actual,
trackEvent: vi.fn(), // mock only trackEvent
};
});
// Spying on object methods
describe("timer integration", () => {
it("uses fake timers for debounce", () => {
vi.useFakeTimers();
const callback = vi.fn();
const debounced = debounce(callback, 300);
debounced();
debounced();
debounced();
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(callback).toHaveBeenCalledOnce();
vi.useRealTimers();
});
});
Snapshot Testing
import { describe, it, expect } from "vitest";
import { renderToString } from "react-dom/server";
import { EmailTemplate } from "./EmailTemplate";
describe("EmailTemplate", () => {
it("matches inline snapshot", () => {
const html = renderToString(
<EmailTemplate userName="Alice" action="signup" />
);
expect(html).toMatchInlineSnapshot(
`"<div><h1>Welcome, Alice!</h1><p>Thanks for signing up.</p></div>"`
);
});
it("matches file snapshot for complex output", () => {
const config = generateConfig({ env: "production", region: "us-east-1" });
expect(config).toMatchSnapshot();
});
});
Workspace Configuration for Monorepos
// vitest.workspace.ts
import { defineWorkspace } from "vitest/config";
export default defineWorkspace([
{
extends: "./vitest.config.ts",
test: {
name: "unit",
include: ["src/**/*.test.ts"],
environment: "node",
},
},
{
extends: "./vitest.config.ts",
test: {
name: "components",
include: ["src/**/*.test.tsx"],
environment: "jsdom",
},
},
{
extends: "./vitest.config.ts",
test: {
name: "integration",
include: ["tests/integration/**/*.test.ts"],
environment: "node",
pool: "forks",
testTimeout: 30_000,
},
},
]);
In-Source Testing
// src/utils/math.ts
export function fibonacci(n: number): number {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
// In-source test block (stripped from production builds by Vite)
if (import.meta.vitest) {
const { describe, it, expect } = import.meta.vitest;
describe("fibonacci", () => {
it("computes first ten values", () => {
const results = Array.from({ length: 10 }, (_, i) => fibonacci(i));
expect(results).toEqual([0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
});
});
}
Benchmarking
// src/utils/search.bench.ts
import { bench, describe } from "vitest";
import { linearSearch, binarySearch } from "./search";
const sorted = Array.from({ length: 10_000 }, (_, i) => i * 2);
describe("search algorithms", () => {
bench("linear search", () => {
linearSearch(sorted, 9998);
});
bench("binary search", () => {
binarySearch(sorted, 9998);
});
});
// Run with: vitest bench
Best Practices
- Reuse your
vite.config.tsby extending it invitest.config.tsso path aliases, plugins, and transforms stay in sync. - Use
vi.clearAllMocks()inbeforeEachto prevent mock state from leaking between tests. - Prefer
vi.mocked()for type-safe access to mock functions rather than casting manually. - Use
vi.useFakeTimers()for anything time-dependent (debounce, throttle, setTimeout) and always callvi.useRealTimers()afterward. - Enable coverage thresholds in CI to prevent regression in test coverage.
- Prefer inline snapshots (
toMatchInlineSnapshot) for small values so the expected output is visible in the test file. - Use workspace configuration to run unit, component, and integration tests with different environments in a single command.
Anti-Patterns
- Do not import from
jest; usevitestimports. The APIs are compatible but the runtime is different. - Avoid mocking everything; if a function is pure and fast, test it directly without mocks.
- Do not use
vi.mockinsideitordescribeblocks; it is hoisted to the top of the file regardless, which causes confusion. - Avoid
toMatchSnapshot()for large objects that change frequently; inline snapshots or explicit assertions are more maintainable. - Do not rely on test execution order within a file; each test should set up its own preconditions.
- Avoid skipping TypeScript in test files; type errors in tests catch real bugs before they reach production.
Install this skill directly: skilldb add testing-services-skills
Related Skills
Cypress
"Cypress: end-to-end/component testing, commands, intercept (network mocking), fixtures, custom commands, CI, retries"
K6
k6: load testing, performance testing, stress testing, soak testing, thresholds, checks, scenarios, browser testing, CI integration
MSW
"MSW (Mock Service Worker): API mocking, request handlers, rest/graphql, browser/node, setupServer, network-level interception"
Pact
Pact: consumer-driven contract testing, provider verification, pact broker, API compatibility, microservice integration testing
Playwright
"Playwright: end-to-end testing, browser automation, selectors, assertions, fixtures, page objects, visual regression, CI, codegen"
Storybook Test
Storybook Test: component testing via play functions, interaction testing, accessibility checks, visual regression, test-runner, portable stories