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

bash
npm install -D vitest

Output: (none — exits 0 on success)

Add scripts to package.json:

json
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:ui": "vitest --ui",
    "coverage": "vitest run --coverage"
  }
}

Configure in vite.config.ts:

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

json
{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

Writing tests

typescript
// 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;
}
typescript
// 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:

text
 ✓ 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.

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

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

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

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

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

typescript
vi.mock("./utils", async (importActual) => {
  const actual = await importActual<typeof import("./utils")>();
  return {
    ...actual,
    formatDate: vi.fn().mockReturnValue("2026-04-26"),
  };
});

Clearing mocks

typescript
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().

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

typescript
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

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

bash
npm install -D @vitest/ui
npx vitest --ui

Output:

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

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

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

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

yaml
- 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.

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

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

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

typescript
// vitest.config.ts
test: {
  environment: "happy-dom",
}

Or per file with a magic comment:

typescript
// @vitest-environment jsdom
import { describe, it, expect } from "vitest";
// ...

happy-dom vs jsdom

jsdomhappy-dom
Maturity10+ years, used by JestNewer, used by SvelteKit
SpeedSlower2-3× faster
Standards complianceHighSlightly lower
MemoryHigherLower

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).

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

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

typescript
// src/__mocks__/api.ts
export const fetchUser = vi.fn().mockResolvedValue({ id: 1, name: "Mock" });

Mocking ES modules with named + default exports

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

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

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

typescript
it("renders a button", () => {
  expect(render(<Button label="OK" />)).toMatchSnapshot();
});

Updating snapshots

bash
# Update all snapshots in the run
npx vitest run --update-snapshots

# Update only matching tests
npx vitest run --update-snapshots renders

Output:

text
 ✓ src/Button.test.tsx (1)
   ✓ renders a button

 Snapshots  1 written
 Test Files  1 passed (1)
      Tests  1 passed (1)

Custom serializers

typescript
// vitest.config.ts
test: {
  snapshotSerializers: ["./src/test/dom-serializer.ts"],
}
typescript
// src/test/dom-serializer.ts
export default {
  test: (val: unknown) => val instanceof Element,
  serialize: (val: Element) => val.outerHTML,
};

Coverage — v8 vs istanbul

v8istanbul
MechanismNode's built-in V8 coverageInserts counters into source via Babel
SetupZero — uses V8 directlyTransform on every test file
SpeedFast (no transform)Slower
Accuracy of branch coverageCoarse (V8 reports ranges)Fine-grained (per branch)
TypeScript supportNative via V8Needs 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

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

bash
npx vitest run --coverage

Output (threshold failure):

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

bash
npm install -D @vitest/browser playwright

Output: (none — exits 0 on success)

typescript
// vitest.config.ts
test: {
  browser: {
    enabled: true,
    provider: "playwright",
    instances: [{ browser: "chromium" }],
    headless: true,
  },
}
bash
npx vitest --browser

Output:

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

PoolWhen to use
threads (default)Worker threads. Fast, low memory overhead.
forksChild processes. Better isolation; required when test code mutates module state in ways that leak between files.
vmThreadsWorker threads + VM context. Like Jest's isolation.
vmForksForks + VM context. Slowest, strongest isolation.
typescript
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

typescript
describe.concurrent("API tests", () => {
  it("GET /users", async () => { /* ... */ });
  it("GET /posts", async () => { /* ... */ });
});

// Or per-test
it.concurrent("fast test", async () => { /* ... */ });

Watch mode

bash
npx vitest                        # watch mode (default in TTY)
npx vitest --watch=false          # one-shot run

Output:

text
 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

PressAction
arun all tests
ffilter to failed tests
tfilter by test name
pfilter by filename
qquit
hhelp

Vitest vs Jest vs Playwright

VitestJestPlaywright Test
ScopeUnit + componentUnit + componentE2E + browser integration
SpeedFast (ESM, esbuild)Slow (Babel transform)N/A — different scope
TypeScriptNativeNeeds ts-jest or BabelNative
ConfigReuses vite.configjest.config.js + Babel + ts-jestplaywright.config.ts
Watch + HMRYesLimitedYes (UI mode)
Mockingvi.mock, vi.fnjest.mock, jest.fnLimited (page-level)
SnapshotsYes (inline + file)YesYes
Browser supportVia @vitest/browser (Playwright)Via jest-playwright (third-party)Native (Chromium/WebKit/Firefox)
Best forVite projects, any modern TSLong-running Jest installsEnd-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.mock not applied — happens when the mock is in a different file than the test. vi.mock calls hoist within the same file only. To share mocks, use __mocks__/ auto-mock or setupFiles.
  • Tests pass solo but fail in suite — shared mutable module state. Set isolate: true (default), or migrate to pool: "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. Add vi.clearAllMocks() in beforeEach or in a setupFiles global hook.
  • Snapshot updates churn in CI — somebody used toMatchSnapshot for a value that changes per run (timestamp, UUID). Either use toMatchInlineSnapshot with 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.
  • fetch is not defined — Node 18+ has fetch globally, but environment: "happy-dom" may not expose it. Add setupFiles: ["whatwg-fetch"] or polyfill in setupFiles.
  • describe.skip/it.skip silently passes — use it.todo("...") for not-yet-written tests so they show up in the summary.
  • TypeScript errors on expect.extend — extend the global type definition; globals: true does not change expect.extend typing.
  • 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

yaml
# .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):

text
 Test Files  12 passed (47)
      Tests  189 passed (752)
   Duration  8.2s

Run a single test by name

bash
# Pattern matches against full test name (describe + it concatenated)
npx vitest run -t "rounds to two decimals"

Output:

text
 ✓ src/format.test.ts (1)
   ✓ rounds to two decimals

Custom assertion via expect.extend

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

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

typescript
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);
  });
});
bash
npx vitest bench

Output:

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

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