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

bash
# 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.

bash
# 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.x is in maintenance, 1.x deprecated.
  • 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) or vitest.config.js (must be ESM if package.json lacks "type": "module").
  • Always a dev dependency — no runtime artefact ships.
  • Tracks Vite versions loosely: vitest@3 works with vite@5 and vite@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-onPurpose
@vitest/uiBrowser-based test UI at /__vitest__/. Adds --ui flag.
@vitest/coverage-v8V8 inline coverage — default since v1. No instrumentation step.
@vitest/coverage-istanbulIstanbul-based coverage — slower but supports branch coverage on TS source.
@vitest/browserReal-browser test mode via Playwright/WebdriverIO/preview.
jsdomSimulated DOM env for component tests (test.environment: "jsdom").
happy-domLighter DOM alternative — faster startup, less coverage of Web APIs.
@testing-library/react / @testing-library/vueComponent testing utilities — framework-agnostic, works with Vitest.
mswNetwork mocking — the standard alongside Vitest for fetch-level mocks.

Alternatives

RunnerTrade-off
jestThe incumbent — huge ecosystem, more plugin coverage. Slower, separate transformer config needed for TS, no native ESM-first design.
mocha + chaiThe classic — flexible, no built-in assertions. Plenty of legacy projects; uncommon for new code.
avaConcurrent-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 testBuilt into Bun. Very fast. Lock-in to Bun runtime; smaller ecosystem.
playwright/testE2E test runner — complementary, not a replacement. Vitest tests units; Playwright tests user flows.

Common gotchas

  1. Globals are off by default. describe, it, expect need explicit imports unless you set test.globals: true in vitest.config.ts and add "types": ["vitest/globals"] to tsconfig.json. Forgetting the types entry means red squiggles even when tests run fine.
  2. 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 */.
  3. Worker isolation vs isolate: false. Each test file runs in a fresh worker by default. Toggling isolate: false for speed shares module state across files — tests that look independent suddenly fail because a previous file's vi.mock leaks.
  4. vi.mock is hoisted. Like Jest's jest.mock, vi.mock("./mod") is statically hoisted above imports. Reference any local variable inside the factory and you get ReferenceError: Cannot access … before initialization — use vi.hoisted() for hoisted setup.
  5. Coverage misses untouched files. V8 coverage only reports lines that ran. Add test.coverage.include to force untouched files into the report — otherwise dead modules show as 100% covered (because zero lines ran, zero failed).
  6. 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".
  7. Concurrent tests share this. test.concurrent runs tests in the same file in parallel; mutating module-level state from a beforeEach produces 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.

typescript
// 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 });
  },
});
typescript
// 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.

typescript
// src/test/setup.ts
import { vi, beforeEach } from "vitest";
import "@testing-library/jest-dom/vitest";

beforeEach(() => {
  vi.stubGlobal("fetch", vi.fn(async () => new Response("{}")));
});
typescript
// 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.

typescript
// 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.

typescript
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

typescript
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":

bash
# 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:

text
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:

yaml
strategy:
  matrix:
    shard: [1, 2, 3, 4]
steps:
  - run: npx vitest run --shard=${{ matrix.shard }}/4

Coverage thresholds as a deploy gate

typescript
// 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

PoolCostWhen
threads (default)Lowest overheadPure tests, no shared state
forksHigher startupTests mutate module state, leak globals
vmThreadsHighestNeed 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

typescript
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:

typescript
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:

typescript
// 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.

typescript
// 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 → ToHighlights
0.x → 1.0Stable API. c8 coverage renamed to @vitest/coverage-v8. Reporter contract changed.
1 → 2Node 18+ required. Workspace mode stabilised. vi.mocked typing tightened. expect.poll introduced.
2 → 3Browser 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.environment magic comment changed parsing — old // @vitest-environment jsdom (no *) still works; some bundlers stripped the comment in 2.x — pin TS comment behaviour with vite-plugin-comments if 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.

  1. --api flag opens a WebSocket for @vitest/ui and IDE integrations. Bound to localhost by default; never expose on a public network.
  2. Snapshot serializers run arbitrary code. A malicious snapshot serializer in a dep can execute on every test run. Pin and review.
  3. Test fixtures with real credentials. Never check .env.test with real prod credentials into git. Use .env.test.local and gitignore.
  4. Browser mode launches a real browser. The browser-mode child process inherits env vars. Strip secrets before npx vitest --browser if running in a shared CI.

Configuration patterns

Workspace mode (monorepo)

typescript
// 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

typescript
test: {
  environmentMatchGlobs: [
    ["src/server/**", "node"],
    ["src/components/**", "happy-dom"],
    ["src/edge/**", "edge-runtime"],
  ],
}

Troubleshooting common errors

  • vitest/globals types not found — add "types": ["vitest/globals"] to tsconfig.json and set test.globals: true in the config. Both required.
  • ReferenceError: Cannot access 'mock' before initialization — referencing a top-level variable inside vi.mock. Wrap the variable in vi.hoisted(() => ...).
  • globals.window is undefinedenvironment: "node" (default) doesn't provide DOM globals. Switch to happy-dom or jsdom.
  • Random timeout failurestestTimeout default is 5000 ms. Bump for slow IO tests; investigate for unit tests (usually a missing await).
  • 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:

ToolWhat it adds
@vitest/uiBrowser-based UI at /__vitest__/ with re-run, filter, and diff viewer
@vitest/browserRun tests in real Chromium / Firefox / WebKit via Playwright
@testing-library/react / …/vue / …/svelteComponent-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 / …-istanbulCoverage providers
vitest-mock-extendedStrongly-typed mocks for deep object trees
vitest-fetch-mockFetch-specific stubbing helpers
@vitest/expectStandalone 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 custom jest.config.cjs plugins, Babel macros, or jest-snapshot extensions 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 bench is fine for micro-benchmarks; for full benchmark suites with statistical analysis use Tinybench directly or mitata.

See also