cheat sheet
Vitest
Vite-native unit test framework with a Jest-compatible API. Covers setup, writing tests, matchers, mocking, timers, async testing, coverage, and UI mode.
Vitest
What it is
Vitest is a Vite-native unit test framework with a Jest-compatible API. It uses native ES modules, runs tests in parallel via worker threads, has first-class TypeScript support (no Babel needed), and reuses your existing vite.config.ts. Because it uses the same transform pipeline as Vite, configuration is minimal for Vite projects.
Install and configure
npm install -D vitest
Output: (none — exits 0 on success)
Add scripts to package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage"
}
}
Configure in vite.config.ts:
// vite.config.ts
import { defineConfig } from "vite";
import { defineConfig as defineVitestConfig } from "vitest/config";
export default defineConfig({
test: {
// Test environment: 'node' (default), 'jsdom', 'happy-dom'
environment: "node",
// Glob patterns for test files
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
// Run setup file before each test file
setupFiles: ["./src/test/setup.ts"],
// Global test APIs (describe, it, expect) without imports
globals: true,
// Coverage configuration
coverage: {
provider: "v8",
reporter: ["text", "json", "html", "lcov"],
exclude: ["node_modules/", "src/test/", "**/*.d.ts"],
},
},
});
If you use globals: true, add to tsconfig.json:
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
Writing tests
// src/math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
// src/math.test.ts
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest";
import { add, divide } from "./math";
describe("add", () => {
it("adds two positive numbers", () => {
expect(add(1, 2)).toBe(3);
});
it("handles negative numbers", () => {
expect(add(-1, -2)).toBe(-3);
});
});
describe("divide", () => {
it("divides two numbers", () => {
expect(divide(10, 2)).toBe(5);
});
it("throws on division by zero", () => {
expect(() => divide(10, 0)).toThrow("Division by zero");
});
});
describe("lifecycle hooks", () => {
beforeAll(() => console.log("once before all tests in this describe"));
afterAll(() => console.log("once after all tests in this describe"));
beforeEach(() => console.log("before each test"));
afterEach(() => console.log("after each test"));
it("runs with hooks", () => {
expect(true).toBe(true);
});
});
Output:
✓ src/math.test.ts (4)
✓ add (2)
✓ adds two positive numbers
✓ handles negative numbers
✓ divide (2)
✓ divides two numbers
✓ throws on division by zero
Test Files 1 passed (1)
Tests 4 passed (4)
Start at 10:00:00
Duration 312ms
Matchers
expect(value) returns a chainable assertion object. Use toBe for primitive strict equality, toEqual for deep structural equality, and toMatchObject for a subset check on objects. Prefix any matcher with .not to assert the inverse.
// Equality
expect(1 + 1).toBe(2); // strict equality (Object.is)
expect({ a: 1 }).toEqual({ a: 1 }); // deep equality
expect({ a: 1, b: 2 }).toStrictEqual({ a: 1 }); // fails — extra key
// Truthiness
expect(1).toBeTruthy();
expect(0).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(1).toBeDefined();
// Numbers
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
expect(5).toBeGreaterThan(4);
expect(5).toBeLessThanOrEqual(5);
// Strings
expect("hello world").toContain("world");
expect("hello").toMatch(/^hell/);
// Arrays / iterables
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect([{ id: 1 }, { id: 2 }]).toContainEqual({ id: 1 });
// Objects
expect({ a: 1, b: 2 }).toHaveProperty("a");
expect({ a: 1, b: 2 }).toHaveProperty("a", 1);
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 }); // subset match
// Errors
expect(() => { throw new Error("boom") }).toThrow();
expect(() => { throw new Error("boom") }).toThrow("boom");
expect(() => { throw new Error("boom") }).toThrow(Error);
// Negation
expect(1).not.toBe(2);
expect(null).not.toBeUndefined();
Snapshots
Snapshots capture a serialised representation of a value and compare it against the stored version on subsequent runs — useful for catching unexpected changes to rendered output or complex data structures. Inline snapshots are stored directly in the test file; file snapshots go to a __snapshots__/ directory and are updated with --update-snapshots.
// Inline snapshot — written into the test file on first run
it("formats a user", () => {
const user = { id: 1, name: "Alice", role: "admin" };
expect(formatUser(user)).toMatchInlineSnapshot(`
"Alice (admin)"
`);
});
// File snapshot — written to __snapshots__/ directory
it("renders a component", () => {
const html = render(<Button label="Click me" />);
expect(html).toMatchSnapshot();
});
// Update snapshots
// npx vitest run --update-snapshots
Mocking
vi.fn() — mock functions
Creates a standalone mock function that records every call, its arguments, and return value. Use .mockReturnValue / .mockResolvedValue to control what it returns, then assert on .toHaveBeenCalledWith to verify it was invoked correctly.
import { vi, describe, it, expect } from "vitest";
it("calls the callback", () => {
const mockFn = vi.fn();
mockFn("hello");
mockFn("world");
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith("hello");
expect(mockFn).toHaveBeenLastCalledWith("world");
});
it("returns a value", () => {
const mockFn = vi.fn().mockReturnValue(42);
expect(mockFn()).toBe(42);
const asyncMock = vi.fn().mockResolvedValue({ data: "ok" });
await expect(asyncMock()).resolves.toEqual({ data: "ok" });
});
vi.spyOn() — spy on real methods
Wraps an existing method on an object to track calls while (by default) still running the real implementation. Call .mockImplementation(() => {}) to suppress the original; call vi.restoreAllMocks() in afterEach to undo the spy and avoid test pollution.
import { vi, it, expect, afterEach } from "vitest";
afterEach(() => vi.restoreAllMocks());
it("spies on console.log", () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
console.log("test message");
expect(spy).toHaveBeenCalledWith("test message");
});
vi.mock() — mock entire modules
Replaces an entire module's exports with mocks for the duration of the test file. Vitest automatically hoists vi.mock() calls to the top of the file (before imports), so the mock is in place before the module-under-test is evaluated.
// vi.mock() is hoisted to the top of the file automatically
import { vi, it, expect } from "vitest";
import { fetchUser } from "./api";
vi.mock("./api", () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: "Alice" }),
}));
it("fetches a user", async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: "Alice" });
expect(fetchUser).toHaveBeenCalledWith(1);
});
vi.importActual() — partial mock
Use inside a vi.mock factory to import the real module, then spread its exports and override only the specific functions you want to mock. This avoids duplicating real implementations in the mock factory.
vi.mock("./utils", async (importActual) => {
const actual = await importActual<typeof import("./utils")>();
return {
...actual,
formatDate: vi.fn().mockReturnValue("2026-04-26"),
};
});
Clearing mocks
vi.clearAllMocks(); // reset call counts and instances
vi.resetAllMocks(); // clearAllMocks + reset return values
vi.restoreAllMocks(); // resetAllMocks + restore original implementations (spies only)
Timer mocking
vi.useFakeTimers() replaces setTimeout, setInterval, and Date with controlled equivalents. Advance time manually with vi.advanceTimersByTime(ms) or skip all pending timers instantly with vi.runAllTimers(). Always restore real timers in afterEach with vi.useRealTimers().
import { vi, it, expect, beforeEach, afterEach } from "vitest";
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("calls after 1 second", () => {
const callback = vi.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
});
it("runs all timers", () => {
const callback = vi.fn();
setTimeout(callback, 5000);
vi.runAllTimers();
expect(callback).toHaveBeenCalled();
});
it("mocks Date", () => {
vi.setSystemTime(new Date("2026-04-26T00:00:00Z"));
expect(new Date().toISOString()).toBe("2026-04-26T00:00:00.000Z");
});
Testing async code
Make the test function async and await the operation under test, or use the .resolves / .rejects matchers on an expect call. Both approaches work equally well; .resolves / .rejects are slightly more concise when you only need to assert the settled value.
import { it, expect } from "vitest";
// Async/await
it("resolves with data", async () => {
const result = await fetchData();
expect(result).toEqual({ id: 1 });
});
// .resolves / .rejects matchers
it("resolves", async () => {
await expect(fetchData()).resolves.toEqual({ id: 1 });
});
it("rejects on bad input", async () => {
await expect(fetchData(null)).rejects.toThrow("Invalid input");
});
// Callback-style with done (rare, prefer async)
it("calls the callback", (done) => {
loadData((err, data) => {
expect(err).toBeNull();
expect(data).toBeDefined();
done();
});
});
Running Vitest
# Watch mode (default in dev)
npx vitest
# Run once and exit (CI)
npx vitest run
# Run tests matching a pattern
npx vitest run math
npx vitest run src/utils
# Watch a specific file
npx vitest watch src/math.test.ts
# Run with verbose output
npx vitest run --reporter=verbose
# Run in browser environment (requires @vitest/browser)
npx vitest --browser
Output: (none — exits 0 on success)
UI mode
npm install -D @vitest/ui
npx vitest --ui
Output:
VITE v5.3.1 ready in 312 ms
➜ Local: http://localhost:51204/__vitest__/
Opens a browser-based UI showing test results, filtering, and re-run controls.
Coverage
Vitest supports two coverage providers: v8 (uses Node's built-in V8 coverage, fast, no instrumentation) and istanbul (inserts counters, more accurate for branch coverage). Configure thresholds in vitest.config.ts to fail CI when coverage drops below acceptable levels.
npm install -D @vitest/coverage-v8
# Run with coverage
npx vitest run --coverage
# Coverage with specific reporters
npx vitest run --coverage --coverage.reporter=lcov
Output:
% Coverage report from v8
-----------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
-----------------------|---------|----------|---------|---------|
All files | 92.31 | 87.50 | 100.0 | 92.31 |
src/math.ts | 100.0 | 100.0 | 100.0 | 100.0 |
src/utils/format.ts | 85.71 | 75.00 | 100.0 | 85.71 |
-----------------------|---------|----------|---------|---------|
vitest.config.ts — standalone config
If you do not use Vite for your app, create a standalone config:
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
include: ["src/**/*.test.ts"],
coverage: {
provider: "v8",
reporter: ["text", "lcov"],
thresholds: {
lines: 80,
branches: 80,
functions: 80,
statements: 80,
},
},
},
});
CI usage (GitHub Actions)
- name: Run tests with coverage
run: npx vitest run --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
vitest.config.ts — full options reference
vitest.config.ts extends vite.config.ts with a test block. If you already have vite.config.ts, prefer extending it so your tests share aliases, plugins, and transforms with the app. The mergeConfig helper is the idiomatic way to compose them.
// vitest.config.ts
import { defineConfig, mergeConfig } from "vitest/config";
import viteConfig from "./vite.config";
export default mergeConfig(
viteConfig,
defineConfig({
test: {
// ── Discovery ─────────────────────────────────────────────
include: ["src/**/*.{test,spec}.{js,ts,jsx,tsx}"],
exclude: ["**/node_modules/**", "**/dist/**", "**/e2e/**"],
includeSource: ["src/**/*.{js,ts}"], // for in-source tests
// ── Environment ───────────────────────────────────────────
environment: "happy-dom", // "node" | "jsdom" | "happy-dom" | "edge-runtime"
environmentMatchGlobs: [
["src/components/**", "happy-dom"],
["src/server/**", "node"],
],
globals: false, // expose describe/it/expect globally
// ── Setup & teardown ──────────────────────────────────────
setupFiles: ["./src/test/setup.ts"],
globalSetup: ["./src/test/global-setup.ts"],
// ── Execution ─────────────────────────────────────────────
pool: "threads", // "threads" | "forks" | "vmThreads" | "vmForks"
poolOptions: {
threads: { singleThread: false, isolate: true, minThreads: 1, maxThreads: 4 },
},
isolate: true,
testTimeout: 5000,
hookTimeout: 10000,
teardownTimeout: 10000,
bail: 0, // stop after N failures (0 = run all)
retry: 0, // retry flaky tests N times
sequence: { shuffle: false, concurrent: false },
// ── Reporting ─────────────────────────────────────────────
reporters: ["default", "html"],
outputFile: { html: "./reports/test-report.html" },
// ── Snapshots ─────────────────────────────────────────────
snapshotFormat: { printBasicPrototype: false },
resolveSnapshotPath: (testPath, snapExtension) =>
testPath.replace(/\.test\.([tj]sx?)$/, `${snapExtension}.$1.snap`),
// ── Coverage ──────────────────────────────────────────────
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov", "json-summary"],
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/**/*.test.ts", "src/**/__mocks__/**"],
thresholds: { lines: 80, branches: 80, functions: 80, statements: 80 },
all: true,
},
},
}),
);
Test discovery — include and includeSource
By default Vitest picks up *.test.* and *.spec.* next to source files. includeSource enables in-source testing — tests written in production files inside if (import.meta.vitest) blocks that are tree-shaken in production.
// src/math.ts — in-source test
export function add(a: number, b: number) { return a + b; }
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest;
it("adds", () => expect(add(1, 2)).toBe(3));
}
To make TypeScript happy, add to vite-env.d.ts:
/// <reference types="vitest/importMeta" />
Test environments
Vitest can swap the JS environment per file. node (default) gives you the real Node globals. jsdom and happy-dom simulate a browser inside Node — they polyfill window, document, localStorage, etc. edge-runtime mimics Vercel/Cloudflare edge runtime.
// vitest.config.ts
test: {
environment: "happy-dom",
}
Or per file with a magic comment:
// @vitest-environment jsdom
import { describe, it, expect } from "vitest";
// ...
happy-dom vs jsdom
| jsdom | happy-dom | |
|---|---|---|
| Maturity | 10+ years, used by Jest | Newer, used by SvelteKit |
| Speed | Slower | 2-3× faster |
| Standards compliance | High | Slightly lower |
| Memory | Higher | Lower |
For new projects use happy-dom. Use jsdom if you hit a missing feature (Selection API edge cases, Canvas, complex form behaviour).
Fixtures — test.extend()
A fixture is a value injected into a test's argument list. Vitest's test.extend creates a typed test function with custom fixtures — useful for shared DB connections, mocked services, or temporary directories. Fixtures support automatic teardown (via the second use parameter pattern).
import { test as base, expect } from "vitest";
interface MyFixtures {
user: { id: number; name: string };
db: { close: () => Promise<void>; query: (sql: string) => unknown };
}
const test = base.extend<MyFixtures>({
user: async ({}, use) => {
const user = { id: 1, name: "Alice" };
await use(user);
// teardown after the test
},
db: async ({}, use) => {
const db = await connectToTestDb();
await use(db);
await db.close(); // teardown
},
});
test("queries with user", async ({ user, db }) => {
const rows = await db.query(`SELECT * FROM accounts WHERE id = ${user.id}`);
expect(rows).toHaveLength(1);
});
Fixtures are lazy — db is only set up if the test actually destructures it.
Mocking — deeper API
The earlier section covered vi.fn, vi.spyOn, vi.mock. Here are the patterns that come up in real codebases.
Hoisting and vi.hoisted
vi.mock(...) calls are hoisted above imports, so the mock is in place before the module under test loads. The factory callback runs before any imports of the test file, so it can't reference variables from the file scope. Use vi.hoisted(...) to hoist a setup value too:
import { vi, it, expect } from "vitest";
const mockUser = vi.hoisted(() => ({ id: 1, name: "Alice" }));
vi.mock("./api", () => ({
fetchUser: vi.fn().mockResolvedValue(mockUser),
}));
import { fetchUser } from "./api";
it("uses mock user", async () => {
expect(await fetchUser(1)).toBe(mockUser);
});
__mocks__ auto-mocking
Place a file at src/__mocks__/api.ts alongside src/api.ts. Calling vi.mock("./api") with no factory will auto-load the __mocks__ version. This is identical to Jest's behaviour and lets you keep mock implementations out of the test file.
// src/__mocks__/api.ts
export const fetchUser = vi.fn().mockResolvedValue({ id: 1, name: "Mock" });
Mocking ES modules with named + default exports
vi.mock("./api", () => ({
default: vi.fn(), // for `import api from "./api"`
fetchUser: vi.fn(), // named exports
__esModule: true,
}));
vi.doMock — non-hoisted mock
Sometimes you need to set up a mock dynamically (e.g. different mocks per test in the same file). vi.doMock is identical to vi.mock but not hoisted, so it must run before the dynamic import of the module under test.
it("mocks per test", async () => {
vi.doMock("./api", () => ({ fetchUser: vi.fn().mockResolvedValue("A") }));
const { fetchUser } = await import("./api");
expect(await fetchUser()).toBe("A");
});
Snapshot deep dive
Inline snapshots
The first run writes the expected value directly into the test file (between the backticks). Subsequent runs compare. Inline snapshots are best for small values where the snapshot is part of the test's readability.
it("formats currency", () => {
expect(formatPrice(1234.5)).toMatchInlineSnapshot(`"$1,234.50"`);
});
File snapshots
Larger values go to __snapshots__/<file>.snap. Use these for serialised React/Vue/Svelte components, JSON API responses, generated SQL, etc.
it("renders a button", () => {
expect(render(<Button label="OK" />)).toMatchSnapshot();
});
Updating snapshots
# Update all snapshots in the run
npx vitest run --update-snapshots
# Update only matching tests
npx vitest run --update-snapshots renders
Output:
✓ src/Button.test.tsx (1)
✓ renders a button
Snapshots 1 written
Test Files 1 passed (1)
Tests 1 passed (1)
Custom serializers
// vitest.config.ts
test: {
snapshotSerializers: ["./src/test/dom-serializer.ts"],
}
// src/test/dom-serializer.ts
export default {
test: (val: unknown) => val instanceof Element,
serialize: (val: Element) => val.outerHTML,
};
Coverage — v8 vs istanbul
| v8 | istanbul | |
|---|---|---|
| Mechanism | Node's built-in V8 coverage | Inserts counters into source via Babel |
| Setup | Zero — uses V8 directly | Transform on every test file |
| Speed | Fast (no transform) | Slower |
| Accuracy of branch coverage | Coarse (V8 reports ranges) | Fine-grained (per branch) |
| TypeScript support | Native via V8 | Needs source maps |
For most projects use v8. Switch to istanbul if you need precise branch coverage or you're hitting V8's range-merging artefacts (some logical/optional-chaining branches aren't counted).
Threshold enforcement
coverage: {
provider: "v8",
thresholds: {
lines: 80,
branches: 75,
functions: 80,
statements: 80,
// Or per-file overrides:
"src/critical/**/*.ts": { lines: 100, branches: 90 },
},
}
A test run with --coverage fails (exit code 1) when any threshold is unmet.
npx vitest run --coverage
Output (threshold failure):
ERROR: Coverage for lines (72%) does not meet global threshold (80%)
ERROR: Coverage for branches (60%) does not meet global threshold (75%)
Browser mode
Vitest can run tests in real browsers (Chromium, Firefox, WebKit) via Playwright or WebDriverIO. Useful when happy-dom/jsdom aren't faithful enough — e.g. testing Canvas, IntersectionObserver, or layout-dependent component behaviour.
npm install -D @vitest/browser playwright
Output: (none — exits 0 on success)
// vitest.config.ts
test: {
browser: {
enabled: true,
provider: "playwright",
instances: [{ browser: "chromium" }],
headless: true,
},
}
npx vitest --browser
Output:
VITE v5.3.1 ready in 312 ms
➜ Browser test session running on Chromium 130
✓ src/Counter.test.tsx (3)
Parallelism, isolation, and pools
Vitest runs test files in parallel by default — each file in its own worker, with the source module re-evaluated per file. Pool options control how workers spawn and isolate.
| Pool | When to use |
|---|---|
threads (default) | Worker threads. Fast, low memory overhead. |
forks | Child processes. Better isolation; required when test code mutates module state in ways that leak between files. |
vmThreads | Worker threads + VM context. Like Jest's isolation. |
vmForks | Forks + VM context. Slowest, strongest isolation. |
test: {
pool: "forks",
poolOptions: {
forks: { singleFork: false, isolate: true, maxForks: 4 },
},
isolate: true,
}
Disabling isolation for speed
If your tests are pure (no shared mutable state), set isolate: false and pool: "threads". This skips re-creating modules per file and can cut test runtime 30-50%.
Running tests concurrently within a file
describe.concurrent("API tests", () => {
it("GET /users", async () => { /* ... */ });
it("GET /posts", async () => { /* ... */ });
});
// Or per-test
it.concurrent("fast test", async () => { /* ... */ });
Watch mode
npx vitest # watch mode (default in TTY)
npx vitest --watch=false # one-shot run
Output:
DEV v1.6.0 /home/alice/project
✓ src/math.test.ts (4)
Test Files 1 passed (1)
Tests 4 passed (4)
PASS Waiting for file changes...
press h to show help, press q to quit
Vitest watches the dependency graph — when src/math.ts changes, every test file that imports it (transitively) re-runs, and nothing else.
Filter while watching
| Press | Action |
|---|---|
a | run all tests |
f | filter to failed tests |
t | filter by test name |
p | filter by filename |
q | quit |
h | help |
Vitest vs Jest vs Playwright
| Vitest | Jest | Playwright Test | |
|---|---|---|---|
| Scope | Unit + component | Unit + component | E2E + browser integration |
| Speed | Fast (ESM, esbuild) | Slow (Babel transform) | N/A — different scope |
| TypeScript | Native | Needs ts-jest or Babel | Native |
| Config | Reuses vite.config | jest.config.js + Babel + ts-jest | playwright.config.ts |
| Watch + HMR | Yes | Limited | Yes (UI mode) |
| Mocking | vi.mock, vi.fn | jest.mock, jest.fn | Limited (page-level) |
| Snapshots | Yes (inline + file) | Yes | Yes |
| Browser support | Via @vitest/browser (Playwright) | Via jest-playwright (third-party) | Native (Chromium/WebKit/Firefox) |
| Best for | Vite projects, any modern TS | Long-running Jest installs | End-to-end browser flows |
The clean answer: Vitest for unit + component, Playwright for E2E. Jest is the right call only when you're stuck on it (e.g. Create-React-App, large legacy codebase) — and even then, vitest-jest-compat makes migration almost mechanical.
Common pitfalls
vi.mocknot applied — happens when the mock is in a different file than the test.vi.mockcalls hoist within the same file only. To share mocks, use__mocks__/auto-mock orsetupFiles.- Tests pass solo but fail in suite — shared mutable module state. Set
isolate: true(default), or migrate topool: "forks"for stricter process isolation. expect(...).toHaveBeenCalledWith(...)passes when it shouldn't — the mock is being called from a different test that didn't clear it. Addvi.clearAllMocks()inbeforeEachor in asetupFilesglobal hook.- Snapshot updates churn in CI — somebody used
toMatchSnapshotfor a value that changes per run (timestamp, UUID). Either usetoMatchInlineSnapshotwith a custom property matcher or refactor to omit volatile fields. - Coverage shows 0% for ESM-only deps — V8 coverage doesn't trace into dynamic imports of CJS-transpiled-to-ESM. Switch to
provider: "istanbul"or exclude the dep. fetchis not defined — Node 18+ has fetch globally, butenvironment: "happy-dom"may not expose it. AddsetupFiles: ["whatwg-fetch"]or polyfill insetupFiles.describe.skip/it.skipsilently passes — useit.todo("...")for not-yet-written tests so they show up in the summary.- TypeScript errors on
expect.extend— extend the global type definition;globals: truedoes not changeexpect.extendtyping. vi.useFakeTimers()doesn't mock Date in Node 22+ — pass{ toFake: ["Date", "setTimeout", "setInterval"] }; some globals must be explicitly opted into.
Real-world recipes
Run tests sharded across CI workers
# .github/workflows/test.yml
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx vitest run --shard=${{ matrix.shard }}/4 --reporter=verbose
Output (shard 2/4):
Test Files 12 passed (47)
Tests 189 passed (752)
Duration 8.2s
Run a single test by name
# Pattern matches against full test name (describe + it concatenated)
npx vitest run -t "rounds to two decimals"
Output:
✓ src/format.test.ts (1)
✓ rounds to two decimals
Custom assertion via expect.extend
// src/test/matchers.ts
import { expect } from "vitest";
expect.extend({
toBeWithinRange(received: number, floor: number, ceiling: number) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () => `expected ${received} ${pass ? "not " : ""}within ${floor}–${ceiling}`,
};
},
});
declare module "vitest" {
interface Assertion<T = unknown> {
toBeWithinRange(floor: number, ceiling: number): T;
}
}
Mock the entire fetch API
// src/test/setup.ts
import { vi, beforeEach } from "vitest";
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn(async (url: string) => {
if (url.endsWith("/users/1")) {
return new Response(JSON.stringify({ id: 1, name: "Alice" }));
}
return new Response(null, { status: 404 });
}));
});
Benchmark with bench
Vitest ships a benchmark runner that uses Tinybench under the hood.
import { bench, describe } from "vitest";
describe("array methods", () => {
bench("for loop", () => {
let sum = 0;
for (let i = 0; i < 1000; i++) sum += i;
});
bench("reduce", () => {
Array.from({ length: 1000 }, (_, i) => i).reduce((a, b) => a + b, 0);
});
});
npx vitest bench
Output:
name hz min max mean p75 p99
· for loop 2,841,001 0.0001 3.2100 0.0004 0.0003 0.0008
· reduce 42,001 0.0218 2.1234 0.0238 0.0240 0.0290
Reuse Vite plugins automatically
When you extend vite.config.ts via mergeConfig, every Vite plugin (e.g. @vitejs/plugin-react, @vitejs/plugin-vue) runs for your tests too. This means JSX, Vue SFC, SVG-as-component imports all work in tests without extra setup.
// vitest.config.ts
import { mergeConfig } from "vitest/config";
import viteConfig from "./vite.config";
export default mergeConfig(viteConfig, {
test: { environment: "happy-dom", setupFiles: ["./src/test/setup.ts"] },
});
See also
- Vite — Vitest reuses Vite's transform pipeline
- Playwright — E2E testing companion
- ESLint — pair with
eslint-plugin-vitest - Biome — replaces ESLint+Prettier alongside Vitest
- TypeScript installation — TS is native in Vitest, no extra config