Vitest is a Vite-native test framework — it reuses your vite.config.ts and runs tests with the same transform pipeline as your app. vi.mock("module") replaces a module entirely; vi.spyOn(obj, "method") intercepts a method call while keeping the original. vi.fn() creates a mock function with call tracking. vi.useFakeTimers() controls setTimeout, setInterval, Date, and performance.now for deterministic time tests. Snapshot testing with toMatchSnapshot() and toMatchInlineSnapshot() locks component output. Coverage with @vitest/coverage-v8 reports line, branch, and function coverage. Workspace config runs multiple test environments in one vitest invocation. Browser mode uses real Playwright or WebdriverIO for DOM tests without JSDOM limitations. test.each parameterizes test cases from arrays or tagged template literals. Claude Code generates Vitest configuration, mock patterns, coverage setups, and the workspace configurations for multi-package repositories.
CLAUDE.md for Vitest
## Vitest Stack
- Version: vitest >= 2.0, @vitest/coverage-v8 >= 2.0
- Mock: vi.mock("path", factory) — hoisted, replaces module in all tests
- Spy: vi.spyOn(object, "method").mockReturnValue(value) — intercept + restore
- Fn: vi.fn().mockImplementation(fn) — assertable, resettable mock
- Timers: vi.useFakeTimers() → vi.advanceTimersByTime(ms) → vi.useRealTimers()
- Workspace: vitest.workspace.ts — run unit + e2e + browser in different envs
- Coverage: @vitest/coverage-v8 — provider: "v8", thresholds in config
- Parameterize: test.each([[input, expected]]) or it.each`template`
Configuration
// vitest.config.ts — root configuration
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: "node", // Default — override per test with @vitest-environment jsdom
setupFiles: ["./src/test/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
exclude: ["node_modules", "dist", "e2e/**"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
reportsDirectory: "./coverage",
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/test/**", "src/**/*.d.ts", "src/**/index.ts"],
thresholds: {
lines: 80,
branches: 75,
functions: 80,
statements: 80,
},
},
// Retry flaky tests in CI
retry: process.env.CI ? 2 : 0,
// Limit parallel workers
pool: "threads",
poolOptions: {
threads: { maxThreads: 4 },
},
},
})
// vitest.workspace.ts — multi-environment workspace
import { defineWorkspace } from "vitest/config"
export default defineWorkspace([
// Unit tests — Node.js environment
{
extends: "./vitest.config.ts",
test: {
name: "unit",
environment: "node",
include: ["src/**/*.unit.test.ts"],
},
},
// Component tests — JSDOM
{
extends: "./vitest.config.ts",
test: {
name: "components",
environment: "jsdom",
setupFiles: ["./src/test/setup-dom.ts"],
include: ["src/**/*.test.tsx"],
},
},
// Browser tests — real Chromium via Playwright
{
test: {
name: "browser",
browser: {
enabled: true,
provider: "playwright",
instances: [{ browser: "chromium" }],
},
include: ["src/**/*.browser.test.ts"],
},
},
])
Module Mocking
// src/lib/__tests__/order-service.test.ts — vi.mock patterns
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { processOrder } from "../order-service"
// Auto-mock entire module — all exports become vi.fn()
vi.mock("../email-service")
vi.mock("../stripe-client")
// Manual factory mock — control implementation
vi.mock("../database", () => ({
db: {
orders: {
create: vi.fn(),
findById: vi.fn(),
update: vi.fn(),
},
products: {
findById: vi.fn(),
update: vi.fn(),
},
},
}))
// Import mocked modules for type-safe access
import { db } from "../database"
import { sendOrderConfirmation } from "../email-service"
import { createPaymentIntent } from "../stripe-client"
describe("processOrder", () => {
beforeEach(() => {
vi.resetAllMocks()
// Setup default mock returns
vi.mocked(db.products.findById).mockResolvedValue({
id: "prod-1",
name: "Widget",
priceCents: 1999,
stock: 10,
})
vi.mocked(db.orders.create).mockResolvedValue({
id: "ord-abc123",
status: "pending",
totalCents: 1999,
createdAt: new Date().toISOString(),
})
vi.mocked(createPaymentIntent).mockResolvedValue({
id: "pi_test_123",
client_secret: "pi_test_123_secret",
})
})
it("creates order and sends confirmation", async () => {
const result = await processOrder({
customerId: "cust-1",
items: [{ productId: "prod-1", quantity: 1 }],
})
expect(result.orderId).toBe("ord-abc123")
// Assert db.orders.create was called with correct args
expect(vi.mocked(db.orders.create)).toHaveBeenCalledWith(
expect.objectContaining({
customerId: "cust-1",
totalCents: 1999,
status: "pending",
})
)
// Assert email was sent
expect(vi.mocked(sendOrderConfirmation)).toHaveBeenCalledOnce()
expect(vi.mocked(sendOrderConfirmation)).toHaveBeenCalledWith(
expect.objectContaining({ orderId: "ord-abc123" })
)
})
it("throws when product has insufficient stock", async () => {
vi.mocked(db.products.findById).mockResolvedValue({
id: "prod-1",
name: "Widget",
priceCents: 1999,
stock: 0, // Out of stock
})
await expect(
processOrder({ customerId: "cust-1", items: [{ productId: "prod-1", quantity: 1 }] })
).rejects.toThrow("Insufficient stock")
// Ensure no order was created
expect(vi.mocked(db.orders.create)).not.toHaveBeenCalled()
})
})
Spies and Timers
// src/lib/__tests__/retry-queue.test.ts — spies + fake timers
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { RetryQueue } from "../retry-queue"
describe("RetryQueue", () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it("retries failed operations with exponential backoff", async () => {
const operation = vi.fn()
.mockRejectedValueOnce(new Error("Network error"))
.mockRejectedValueOnce(new Error("Network error"))
.mockResolvedValueOnce({ success: true })
const queue = new RetryQueue({ maxRetries: 3, baseDelayMs: 1000 })
const resultPromise = queue.enqueue(operation)
// First attempt fires immediately
await vi.runAllMicrotasksAsync()
expect(operation).toHaveBeenCalledTimes(1)
// Advance 1 second — first retry (1000ms backoff)
await vi.advanceTimersByTimeAsync(1000)
expect(operation).toHaveBeenCalledTimes(2)
// Advance 2 more seconds — second retry (2000ms backoff)
await vi.advanceTimersByTimeAsync(2000)
expect(operation).toHaveBeenCalledTimes(3)
const result = await resultPromise
expect(result).toEqual({ success: true })
})
it("rejects after max retries exhausted", async () => {
const operation = vi.fn().mockRejectedValue(new Error("Persistent error"))
const queue = new RetryQueue({ maxRetries: 2, baseDelayMs: 100 })
const resultPromise = queue.enqueue(operation)
await vi.runAllTimersAsync()
await expect(resultPromise).rejects.toThrow("Persistent error")
expect(operation).toHaveBeenCalledTimes(3) // 1 initial + 2 retries
})
it("uses current Date for timestamp on entry creation", () => {
vi.setSystemTime(new Date("2024-01-15T10:00:00Z"))
const queue = new RetryQueue({ maxRetries: 3, baseDelayMs: 1000 })
const entry = queue.createEntry(() => Promise.resolve())
expect(entry.createdAt).toEqual(new Date("2024-01-15T10:00:00Z"))
})
})
Parameterized Tests
// src/lib/__tests__/price-calculator.test.ts — test.each
import { describe, it, expect } from "vitest"
import { calculateDiscount, formatCents, parseOrderStatus } from "../utils"
describe("calculateDiscount", () => {
// Array form: [input, expected, testName]
it.each([
[10000, 10, 9000, "10% off $100"],
[10000, 0, 10000, "0% off"],
[10000, 100, 0, "100% off"],
[999, 15, 849, "15% off $9.99 rounds correctly"],
])("calculates %s cents with %s%% discount → %s cents (%s)", (
totalCents,
discountPercent,
expected
) => {
expect(calculateDiscount(totalCents, discountPercent)).toBe(expected)
})
})
describe("formatCents", () => {
// Tagged template form
it.each`
cents | currency | expected
${0} | ${"USD"} | ${"$0.00"}
${999} | ${"USD"} | ${"$9.99"}
${10000} | ${"USD"} | ${"$100.00"}
${1234} | ${"EUR"} | ${"€12.34"}
`("formats $cents $currency → $expected", ({ cents, currency, expected }) => {
expect(formatCents(cents, currency)).toBe(expected)
})
})
describe("parseOrderStatus", () => {
it.each([
["PENDING", "pending"],
["pending", "pending"],
[" Shipped ", "shipped"],
["DELIVERED", "delivered"],
])("normalizes %s → %s", (input, expected) => {
expect(parseOrderStatus(input)).toBe(expected)
})
it.each(["unknown", "invalid", ""])(
"throws for invalid status: %s",
(status) => {
expect(() => parseOrderStatus(status)).toThrow("Invalid order status")
}
)
})
Snapshot Tests
// src/lib/__tests__/email-template.test.ts — snapshots
import { describe, it, expect } from "vitest"
import { generateOrderEmail } from "../email-template"
describe("generateOrderEmail", () => {
it("renders order confirmation HTML", () => {
const html = generateOrderEmail({
orderId: "ord-test-123",
customerName: "Alice",
items: [{ name: "Widget Pro", quantity: 1, priceCents: 2999 }],
totalCents: 2999,
})
// First run creates snapshot, subsequent runs compare
expect(html).toMatchSnapshot()
})
it("renders inline without snapshot file", () => {
const subject = generateOrderEmail({ orderId: "ord-test", customerName: "Bob" }).subject
expect(subject).toMatchInlineSnapshot(`"Order ord-test confirmed — thanks Bob!"`)
})
})
For the Jest alternative when migrating from Jest is not feasible — large existing test suites with Jest-specific matchers like jest.spyOn, JSX transform, or snapshot serializers — the Testing Library guide covers the React component testing patterns that work identically in both. For Playwright E2E testing that complements Vitest unit tests by validating full user flows in real browsers, see the Playwright guide for page objects and browser automation. The Claude Skills 360 bundle includes Vitest skill sets covering mocking strategies, coverage, and workspace configuration. Start with the free tier to try Vitest test generation.