cheat sheet
vitest
Package-level reference for vitest on npm — install variants, vite peer-dep, UI and coverage add-ons, environments, and alternatives.
vitest
What it is
vitest is a Vite-native test runner built by the Vite team. It reuses Vite's transformer pipeline and config, so TS, JSX, CSS, and path aliases work the same in tests as in your app — no parallel jest + ts-jest + Babel config required. The API is intentionally Jest-compatible (describe/it/expect, mocks, snapshots) so migration is a near drop-in.
It runs by default in a multi-threaded worker pool, watch mode is HMR-backed (changed test files rerun, dependent files trigger their tests), and the bundled UI gives a web-based test browser.
Install
# npm / pnpm / yarn / bun
npm install -D vitest
pnpm add -D vitest
yarn add -D vitest
bun add -d vitest
Output: vitest binary on PATH under node_modules/.bin/vitest.
# Common add-ons
npm install -D @vitest/ui # browser UI for tests
npm install -D @vitest/coverage-v8 # V8-native coverage (default, fastest)
npm install -D @vitest/coverage-istanbul # Istanbul coverage (more accurate, slower)
npm install -D jsdom # DOM environment for component tests
npm install -D happy-dom # lighter DOM alt to jsdom
Output: add-ons enabled by referencing in vitest.config.ts (test.ui, test.coverage.provider, test.environment).
Versioning & Node support
- Current major line is
3.x(released late 2024 — landed browser mode stabilisation, project workspaces).2.xis in maintenance,1.xdeprecated. - Recent releases require Node 18.17+ or 20+. EOL Node versions are dropped on each major.
- ESM-only package — config file can be
vitest.config.ts(TS, preferred) orvitest.config.js(must be ESM ifpackage.jsonlacks"type": "module"). - Always a dev dependency — no runtime artefact ships.
- Tracks Vite versions loosely:
vitest@3works withvite@5andvite@6/vite@7. Pin the pair carefully on each upgrade.
Package metadata
- Maintainer: vitejs / vitest core team (Anthony Fu, Sheremet Vladislav, et al.)
- Project home: github.com/vitest-dev/vitest
- Docs: vitest.dev
- npm: npmjs.com/package/vitest
- License: MIT
- First released: late 2021
- Downloads: millions per week and rising — now the default test runner in most new Vite-based templates.
Peer dependencies & extras
vitest lists vite as a peer dep. If you don't already have Vite installed, npm/pnpm will warn — install vite alongside even for pure-Node test projects.
| Add-on | Purpose |
|---|---|
@vitest/ui | Browser-based test UI at /__vitest__/. Adds --ui flag. |
@vitest/coverage-v8 | V8 inline coverage — default since v1. No instrumentation step. |
@vitest/coverage-istanbul | Istanbul-based coverage — slower but supports branch coverage on TS source. |
@vitest/browser | Real-browser test mode via Playwright/WebdriverIO/preview. |
jsdom | Simulated DOM env for component tests (test.environment: "jsdom"). |
happy-dom | Lighter DOM alternative — faster startup, less coverage of Web APIs. |
@testing-library/react / @testing-library/vue | Component testing utilities — framework-agnostic, works with Vitest. |
msw | Network mocking — the standard alongside Vitest for fetch-level mocks. |
Alternatives
| Runner | Trade-off |
|---|---|
| jest | The incumbent — huge ecosystem, more plugin coverage. Slower, separate transformer config needed for TS, no native ESM-first design. |
| mocha + chai | The classic — flexible, no built-in assertions. Plenty of legacy projects; uncommon for new code. |
| ava | Concurrent-by-default, isolated processes per file. Simpler than Jest; tiny ecosystem next to Vitest/Jest. |
| node:test (stdlib) | Built into Node 20+. Zero dependency, zero config. Good for libraries; lacks watch mode, UI, snapshot system. |
| bun test | Built into Bun. Very fast. Lock-in to Bun runtime; smaller ecosystem. |
| playwright/test | E2E test runner — complementary, not a replacement. Vitest tests units; Playwright tests user flows. |
Common gotchas
- Globals are off by default.
describe,it,expectneed explicit imports unless you settest.globals: trueinvitest.config.tsand add"types": ["vitest/globals"]totsconfig.json. Forgetting the types entry means red squiggles even when tests run fine. - Environment is per-config, not per-file by default. A single
test.environment: "node"applies to everything. Switch per file with a docblock at the top:/** @vitest-environment jsdom */. - Worker isolation vs
isolate: false. Each test file runs in a fresh worker by default. Togglingisolate: falsefor speed shares module state across files — tests that look independent suddenly fail because a previous file'svi.mockleaks. vi.mockis hoisted. Like Jest'sjest.mock,vi.mock("./mod")is statically hoisted above imports. Reference any local variable inside the factory and you getReferenceError: Cannot access … before initialization— usevi.hoisted()for hoisted setup.- Coverage misses untouched files. V8 coverage only reports lines that ran. Add
test.coverage.includeto force untouched files into the report — otherwise dead modules show as 100% covered (because zero lines ran, zero failed). - Snapshot files are committed.
__snapshots__/belongs in git. Reviewers should diff them — silent snapshot drift is the most common cause of "tests pass but the UI broke". - Concurrent tests share
this.test.concurrentruns tests in the same file in parallel; mutating module-level state from abeforeEachproduces flaky results. Use fixtures (test.extend) to scope state per test.
Real-world recipes
Worked examples for the patterns you hit on every greenfield Vitest project — typed fixtures, a shared setup file, and database-backed tests.
Typed fixture with teardown
Vitest's test.extend builds a fixture-aware test fn. Use the second argument (use) to inject the value; statements after use(value) run as teardown.
// tests/fixtures.ts
import { test as base } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
interface Fixtures { tmp: string }
export const test = base.extend<Fixtures>({
tmp: async ({}, use) => {
const dir = mkdtempSync(join(tmpdir(), "vt-"));
await use(dir);
rmSync(dir, { recursive: true, force: true });
},
});
// tests/writer.test.ts
import { expect } from "vitest";
import { test } from "./fixtures";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
test("writes a file to the tmp dir", async ({ tmp }) => {
const p = join(tmp, "out.txt");
writeFileSync(p, "hello");
expect(p).toContain(tmp);
});
Shared setup file
A setup file runs once per test file (before any test); use it to wire global mocks, polyfills, or matchers.
// src/test/setup.ts
import { vi, beforeEach } from "vitest";
import "@testing-library/jest-dom/vitest";
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn(async () => new Response("{}")));
});
// vitest.config.ts
test: { setupFiles: ["./src/test/setup.ts"] }
Database-backed integration tests
Run each test file against a fresh schema. The pool: "forks" setting gives strong process isolation so a leaked connection from one file doesn't poison the next.
// src/test/db.ts — fixture pattern
import { test as base } from "vitest";
import { PrismaClient } from "@prisma/client";
export const test = base.extend<{ db: PrismaClient }>({
db: async ({}, use) => {
const db = new PrismaClient({ datasources: { db: { url: process.env.TEST_DATABASE_URL } } });
await db.$transaction([db.user.deleteMany(), db.post.deleteMany()]);
await use(db);
await db.$disconnect();
},
});
Inline snapshots for tiny outputs
Inline snapshots keep the expected value next to the assertion — best when the snapshot is small and reading the test should reveal the contract.
import { test, expect } from "vitest";
import { formatPrice } from "./format";
test("formats USD prices", () => {
expect(formatPrice(1234.5, "USD")).toMatchInlineSnapshot(`"$1,234.50"`);
expect(formatPrice(0.1 + 0.2, "USD")).toMatchInlineSnapshot(`"$0.30"`);
});
Run npx vitest -u to update inline snapshots in place. Reviewers see the diff in the PR.
Custom matcher
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;
}
}
Production deployment
Vitest itself doesn't ship runtime artefacts — but it's a CI workhorse.
CI parity with local
Match Node version, pool, and isolation flags between local and CI. Mismatches surface as "passes locally, fails in CI":
# locally
node --version # v20.11.0
npx vitest run --pool=threads --isolate
# CI (.github/workflows/test.yml)
- uses: actions/setup-node@v4
with: { node-version: "20.11.0", cache: "npm" }
- run: npx vitest run --pool=threads --isolate --reporter=junit --outputFile=junit.xml
Output:
v20.11.0
✓ src/utils.test.ts (4 tests) 18ms
✓ src/api.test.ts (7 tests) 92ms
Test Files 2 passed (2)
Tests 11 passed (11)
Shard across workers
The --shard=N/M flag deterministically partitions test files. Combine with a matrix to parallelise across CI runners:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx vitest run --shard=${{ matrix.shard }}/4
Coverage thresholds as a deploy gate
// vitest.config.ts
test: {
coverage: {
provider: "v8",
thresholds: { lines: 80, branches: 75, functions: 80, statements: 80 },
reporter: ["text", "lcov", "json-summary"],
},
}
--coverage exits non-zero when thresholds are unmet — wire it as a required check on PRs.
Performance tuning
Vitest is fast by default, but large suites benefit from explicit knob-twisting.
Pool choice
| Pool | Cost | When |
|---|---|---|
threads (default) | Lowest overhead | Pure tests, no shared state |
forks | Higher startup | Tests mutate module state, leak globals |
vmThreads | Highest | Need Jest-like VM isolation |
Switching from threads to forks on a 500-file suite typically adds 5-15s of startup but eliminates entire classes of flake.
Disable isolation for speed
test: { isolate: false, pool: "threads" }
This skips re-evaluating modules per file. Cuts runtime 30-50% on suites that don't mutate shared state. Adopt incrementally — one cross-file leak makes everything fail.
concurrent for I/O-bound tests
describe.concurrent runs tests inside one file in parallel:
describe.concurrent("HTTP fetches", () => {
it("GET /a", async () => { /* ... */ });
it("GET /b", async () => { /* ... */ });
});
For CPU-bound tests this hurts; for I/O-bound it's a 2-4× win.
Pre-bundle deps with optimizeDeps
The same Vite knob applies. If your test setup imports an expensive CJS dep (e.g. pg, mysql2), pre-bundle it:
// vitest.config.ts
optimizeDeps: { include: ["pg", "ioredis"] }
ESM/CJS interop & bundling
Vitest is ESM-first since 1.0. The config file can be .ts, .mts, or .js (ESM if "type": "module"). Imports inside test files follow Node's resolution — vi.mock works on both CJS and ESM specifiers.
// CJS module — vi.mock works the same
vi.mock("./legacy-cjs-helper");
// ESM module — `vi.importActual` must use the typed generic
const actual = await vi.importActual<typeof import("./mod")>("./mod");
For CJS-only deps that Vite's pre-bundler mis-detects exports for, use server.deps.optimizer.web.include in vitest.config.ts (the same knob as Vite's optimizeDeps.include but namespaced under test).
Version migration guide
| From → To | Highlights |
|---|---|
| 0.x → 1.0 | Stable API. c8 coverage renamed to @vitest/coverage-v8. Reporter contract changed. |
| 1 → 2 | Node 18+ required. Workspace mode stabilised. vi.mocked typing tightened. expect.poll introduced. |
| 2 → 3 | Browser mode multi-instance support. Reporters API rewrite. vi.hoisted semantics clarified. pool default behaviour favours threads more aggressively. Deprecated --single-thread (use --pool=threads --poolOptions.threads.singleThread). |
Common migration friction
- The
test.environmentmagic comment changed parsing — old// @vitest-environment jsdom(no*) still works; some bundlers stripped the comment in 2.x — pin TS comment behaviour withvite-plugin-commentsif you depend on it. vi.fn().mockReturnValue(...)typing in 3.x infers tighter return types from the consuming type — TS may now flag previously-passing tests as type errors. Cast explicitly:vi.fn<() => string>().mockReturnValue("x").
Security considerations
Vitest runs your test code with full Node permissions. Treat it like running the app itself — don't load fixtures from untrusted sources, don't expose --api over the network in CI.
--apiflag opens a WebSocket for@vitest/uiand IDE integrations. Bound to localhost by default; never expose on a public network.- Snapshot serializers run arbitrary code. A malicious snapshot serializer in a dep can execute on every test run. Pin and review.
- Test fixtures with real credentials. Never check
.env.testwith real prod credentials into git. Use.env.test.localand gitignore. - Browser mode launches a real browser. The browser-mode child process inherits env vars. Strip secrets before
npx vitest --browserif running in a shared CI.
Configuration patterns
Workspace mode (monorepo)
// vitest.workspace.ts
export default [
"packages/api/vitest.config.ts",
"packages/ui/vitest.config.ts",
{ test: { name: "shared", root: "./packages/shared" } },
];
npx vitest then runs all workspaces; --project ui narrows.
Per-environment configs
test: {
environmentMatchGlobs: [
["src/server/**", "node"],
["src/components/**", "happy-dom"],
["src/edge/**", "edge-runtime"],
],
}
Troubleshooting common errors
vitest/globalstypes not found — add"types": ["vitest/globals"]totsconfig.jsonand settest.globals: truein the config. Both required.ReferenceError: Cannot access 'mock' before initialization— referencing a top-level variable insidevi.mock. Wrap the variable invi.hoisted(() => ...).globals.window is undefined—environment: "node"(default) doesn't provide DOM globals. Switch tohappy-domorjsdom.- Random timeout failures —
testTimeoutdefault is 5000 ms. Bump for slow IO tests; investigate for unit tests (usually a missingawait). - Coverage shows 0% for ESM-only deps — V8 coverage can't trace into dynamically imported ESM-from-CJS interop layers. Switch to
provider: "istanbul"or exclude.
Ecosystem integrations
Vitest sits at the centre of the modern Vite-flavoured testing stack. The plugins and add-ons you'll actually reach for:
| Tool | What it adds |
|---|---|
@vitest/ui | Browser-based UI at /__vitest__/ with re-run, filter, and diff viewer |
@vitest/browser | Run tests in real Chromium / Firefox / WebKit via Playwright |
@testing-library/react / …/vue / …/svelte | Component-test queries — framework-agnostic, paired with Vitest's DOM environments |
msw (Mock Service Worker) | Network mocking via service-worker / Node interceptor — preferred over global fetch stubs for realistic flows |
@vitest/coverage-v8 / …-istanbul | Coverage providers |
vitest-mock-extended | Strongly-typed mocks for deep object trees |
vitest-fetch-mock | Fetch-specific stubbing helpers |
@vitest/expect | Standalone expect — usable outside Vitest (Storybook, Playwright fixtures) |
Workspace mode lets a monorepo run one vitest command across every package, with per-package configs. The @vitest/ui browser pane navigates across the whole workspace.
When NOT to use this
Vitest is the default new choice for any Vite-based or modern TS project — but a few cases push back to alternatives:
- Legacy Jest mock-heavy projects. Vitest's Jest-compat layer covers 90%+ of
jest.*APIs, but customjest.config.cjsplugins, Babel macros, orjest-snapshotextensions may not port. Migrate incrementally — convert one file at a time and keep both runners in CI until the last Jest file lands. - Pure Node CLI with no Vite.
node:test(stdlib) needs zero deps and zero config — for a 50-line CLI, it's lighter than pulling in Vitest + Vite. Pick Vitest as soon as you want watch mode, HMR-backed reruns, or coverage. - E2E flows. Vitest tests units and components; for full browser flows (auth, multi-page navigation, visual regression) use Playwright instead.
- Performance benchmarks. Vitest's
benchis fine for micro-benchmarks; for full benchmark suites with statistical analysis use Tinybench directly ormitata.
See also
- JavaScript: vitest — full API, fixtures, mocks, snapshots
- JavaScript: vite — peer build tool
- Concept: API — test contracts and public-surface stability
- Packages: npm-vite — companion bundler
- Packages: npm-playwright — E2E layer above unit tests