
Vitest
- 184 installs
- 22 repo stars
- Updated August 1, 2026
- itechmeat/llm-code
Configure and write Vitest unit and integration tests for TypeScript/JavaScript codebases with mocks, coverage, and CI-friendly test scripts.
About
Provides practical Vitest guidance for shipping reliable JavaScript and TypeScript apps, including project setup, test file organization, mocking strategies, coverage thresholds, and patterns for frontend components and backend modules.
- Vitest setup for TS and JS projects
- Unit, component, and integration patterns
- Mocking, fixtures, and coverage config
- CI-ready npm test script conventions
- Fast Vite-native test execution
Vitest by the numbers
- 184 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #828 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/itechmeat/llm-code --skill vitestAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 184 |
|---|---|
| repo stars | ★ 22 |
| Last updated | August 1, 2026 |
| Repository | itechmeat/llm-code ↗ |
What it does
Configure and write Vitest unit and integration tests for TypeScript/JavaScript codebases with mocks, coverage, and CI-friendly test scripts.
Files
Vitest
Next generation testing framework powered by Vite.
Quick Navigation
- Test API - test, describe, hooks
- Expect API - matchers and assertions
- Mocking - vi.fn, vi.mock, fake timers
- Configuration - vitest.config.ts options
- CLI - command line reference
- Browser Mode - real browser testing
When to Use
- Testing Vite-based applications (shared config)
- Need Jest-compatible API with native ESM support
- Component testing in real browser
- Fast watch mode with HMR
- TypeScript testing without extra config
- Parallel test execution
Installation
Install: npm install -D vitest. Requires Vite >=v6.0.0, Node >=v20.0.0.
Release Highlights (4.1.0 -> 4.1.6)
- New control-flow hooks:
aroundEachandaroundAll. - Test metadata expands with tags,
meta, and improvedtest.extendtype inference. - CLI adds
--detect-async-leaks, richer--updatemodes, and static collection forvitest list. - Browser mode grows Playwright persistent contexts,
userEvent.wheel, and stronger trace/artifact handling. - Mocking/timers add disposable
doMock(),mockThrow/mockThrowOnce, andsetTickMode. 4.1.4adds experimental ARIA snapshots,filterMetafor the JSON reporter, and exposesassertionas a public experimental field.4.1.5adds coverageinstrumentersupport and improves reporter/UI behavior around large snapshots and HTML output.4.1.6tightens browser screenshot path resolution and fixessequence.concurrenthandling for concurrent test scheduling.
Quick Start
// sum.js
export function sum(a, b) {
return a + b;
}// sum.test.js
import { expect, test } from "vitest";
import { sum } from "./sum.js";
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3);
});// package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"coverage": "vitest run --coverage"
}
}Configuration
// vitest.config.ts (recommended)
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true, // Enable global test APIs
environment: "jsdom", // Browser-like environment
include: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
coverage: {
provider: "v8",
reporter: ["text", "html"],
},
},
});Or extend Vite config:
// vite.config.ts
/// <reference types="vitest/config" />
import { defineConfig } from "vite";
export default defineConfig({
test: {
// test options
},
});Test File Naming
By default, tests must contain .test. or .spec. in filename:
sum.test.jssum.spec.ts__tests__/sum.js
Key Commands
# Watch mode (default)
vitest
# Single run
vitest run
# With coverage
vitest run --coverage
# Filter by file/test name
vitest sum
vitest -t "should add"
# UI mode
vitest --ui
# Browser tests
vitest --browser.enabledCommon Patterns
Basic Test
import { describe, it, expect, beforeEach } from "vitest";
describe("Calculator", () => {
let calc: Calculator;
beforeEach(() => {
calc = new Calculator();
});
it("adds numbers", () => {
expect(calc.add(1, 2)).toBe(3);
});
it("throws on invalid input", () => {
expect(() => calc.add("a", 1)).toThrow();
});
});Mocking
import { vi, expect, test } from "vitest";
import { fetchUser } from "./api";
vi.mock("./api", () => ({
fetchUser: vi.fn(),
}));
test("uses mocked API", async () => {
vi.mocked(fetchUser).mockResolvedValue({ name: "John" });
const user = await fetchUser(1);
expect(fetchUser).toHaveBeenCalledWith(1);
expect(user.name).toBe("John");
});Snapshot Testing
import { expect, test } from "vitest";
test("matches snapshot", () => {
const result = generateConfig();
expect(result).toMatchSnapshot();
});
// Inline snapshot (auto-updates)
test("inline snapshot", () => {
expect({ foo: "bar" }).toMatchInlineSnapshot();
});Async Testing
import { expect, test } from "vitest";
test("async/await", async () => {
const result = await fetchData();
expect(result).toBeDefined();
});
test("resolves", async () => {
await expect(Promise.resolve("ok")).resolves.toBe("ok");
});
test("rejects", async () => {
await expect(Promise.reject(new Error())).rejects.toThrow();
});Fake Timers
import { vi, expect, test, beforeEach, afterEach } from "vitest";
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test("advances time", () => {
const callback = vi.fn();
setTimeout(callback, 1000);
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});Jest Migration
Most Jest code works with minimal changes:
- import { jest } from '@jest/globals'
+ import { vi } from 'vitest'
- jest.fn()
+ vi.fn()
- jest.mock('./module')
+ vi.mock('./module')
- jest.useFakeTimers()
+ vi.useFakeTimers()Key differences:
- Use
viinstead ofjest - Globals not enabled by default (add
globals: true) vi.mockis hoisted (usevi.doMockfor non-hoisted)- No
jest.requireActual(usevi.importActual)
Environment Selection
// vitest.config.ts
{
test: {
environment: 'jsdom', // or 'happy-dom', 'node', 'edge-runtime'
}
}
// Per-file (docblock at top)
/** @vitest-environment jsdom */TypeScript
// tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}References
See references/ directory for detailed documentation on:
- Test API and hooks
- All expect matchers
- Mocking functions and modules
- Configuration options
- CLI commands
- Browser mode testing
Links
Vitest Test API Reference
Core testing functions and hooks.
Test Functions
test / it
import { expect, test } from "vitest";
test("should work", () => {
expect(Math.sqrt(4)).toBe(2);
});
// With timeout (default 5s, configurable via testTimeout)
test("async operation", async () => {
// ...
}, 10000);
// With options object
test("with options", { timeout: 10000, retry: 2 }, () => {
// ...
});test.skip / test.only / test.todo
test.skip("skipped test", () => {});
test.only("only this runs", () => {});
test.todo("implement later");
// Dynamic skip
test("conditional", (context) => {
context.skip(condition, "optional reason");
});test.skipIf / test.runIf
const isDev = process.env.NODE_ENV === "development";
test.skipIf(isDev)("prod only", () => {});
test.runIf(isDev)("dev only", () => {});test.concurrent / test.sequential
// Run tests in parallel
test.concurrent("concurrent 1", async () => {});
test.concurrent("concurrent 2", async () => {});
// Force sequential in concurrent context
test.sequential("must run alone", async () => {});test.each / test.for
// Parameterized tests
test.each([
[1, 1, 2],
[1, 2, 3],
])("add(%i, %i) -> %i", (a, b, expected) => {
expect(a + b).toBe(expected);
});
// With object parameters
test.each([
{ a: 1, b: 1, expected: 2 },
{ a: 1, b: 2, expected: 3 },
])("add($a, $b) -> $expected", ({ a, b, expected }) => {
expect(a + b).toBe(expected);
});
// test.for - keeps array intact (recommended)
test.for([
[1, 1, 2],
[1, 2, 3],
])("add(%i, %i) -> %i", ([a, b, expected]) => {
expect(a + b).toBe(expected);
});test.extend (Custom Fixtures)
const myTest = test.extend({
todos: async ({ task }, use) => {
const todos = [1, 2, 3];
await use(todos);
// cleanup
},
});
myTest("with fixture", ({ todos }) => {
expect(todos.length).toBe(3);
});describe (Suites)
describe("group", () => {
test("test 1", () => {});
test("test 2", () => {});
});
// Nested
describe("outer", () => {
describe("inner", () => {
test("nested test", () => {});
});
});describe modifiers
describe.skip('skipped suite', () => {})
describe.only('only this suite', () => {})
describe.todo('implement later')
describe.concurrent('parallel tests', () => {})
describe.sequential('sequential tests', () => {})
describe.shuffle('random order', () => {})
describe.each([...])('parameterized', (params) => {})Setup and Teardown
import { beforeAll, afterAll, beforeEach, afterEach } from "vitest";
beforeAll(async () => {
// once before all tests
await setupDatabase();
// cleanup function (equivalent to afterAll)
return async () => {
await teardownDatabase();
};
});
afterAll(async () => {
// once after all tests
});
beforeEach(async () => {
// before each test
});
afterEach(async () => {
// after each test
});aroundEach / aroundAll (v4.1.0)
Use aroundEach or aroundAll when setup and teardown need to be expressed as one wrapped lifecycle instead of split hooks.
- Prefer them for resource scopes that must guarantee paired setup/cleanup.
- If an
around*hook times out or throws, treat it as test infrastructure failure, not as something to paper over with retries.
Test Hooks (Inside Test)
import { test, onTestFinished, onTestFailed } from "vitest";
test("with cleanup", () => {
const db = connectDb();
onTestFinished(() => db.close());
onTestFailed(({ task }) => {
console.log("Failed:", task.result.errors);
});
db.query("SELECT * FROM users");
});
// For concurrent tests, use context
test.concurrent("concurrent", ({ onTestFinished }) => {
onTestFinished(() => cleanup());
});Benchmark
import { bench, describe } from "vitest";
bench(
"normal sorting",
() => {
const x = [1, 5, 4, 2, 3];
x.sort((a, b) => a - b);
},
{ time: 1000, iterations: 100 },
);
describe("benchmarks", () => {
bench.skip("skipped", () => {});
bench.only("only this", () => {});
bench.todo("implement later");
});Test Options
interface TestOptions {
timeout?: number; // max execution time (default 5s)
retry?: number; // retry count on failure (default 0)
repeats?: number; // repeat count even on success
meta?: Record<string, unknown>; // custom metadata (v4.1.0)
}Tags and metadata (v4.1.0)
- Use tags for coarse-grained slicing/reporting.
- Use
metafor machine-readable annotations consumed by tooling or reporters.
Important Notes
- Concurrent tests: Use
expectfrom context for snapshots
test.concurrent("test", async ({ expect }) => {
expect(foo).toMatchSnapshot();
});- Type checker mode:
.concurrent,.skipIf,.runIf,.each,.failsnot supported - Default timeout: 5 seconds (configurable via
testTimeoutconfig)
Vitest Browser Mode Reference
Run tests in real browsers for accurate DOM testing.
Why Browser Mode?
- Real browser environment - No simulation (jsdom/happy-dom) discrepancies
- Native browser APIs - Access to window, document, and browser-specific features
- Component testing - Test React, Vue, Svelte, etc. in actual browsers
- Visual regression - Screenshot and compare UI changes
Installation
Quick Setup
npx vitest init browserManual Setup
# For Playwright (recommended)
npm install -D vitest @vitest/browser-playwright
# For WebdriverIO
npm install -D vitest @vitest/browser-webdriverio
# For preview only (not for CI)
npm install -D vitest @vitest/browser-previewConfiguration
Basic Setup
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { playwright } from "@vitest/browser-playwright";
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: "chromium" }],
},
},
});With Framework (React Example)
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import { playwright } from "@vitest/browser-playwright";
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: "chromium" }],
},
},
});Multi-Browser Testing
{
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
{ browser: 'firefox' },
{ browser: 'webkit' },
],
},
}Headless Mode
{
browser: {
enabled: true,
provider: playwright(),
headless: true, // Run without UI
instances: [{ browser: 'chromium' }],
},
}v4.1.0 browser-mode additions
- Playwright provider can use persistent contexts.
launchOptionscan be combined withconnectOptionsfor more advanced browser connection setups.userEvent.wheelis available for wheel/scroll interactions.- Failure screenshots and trace-oriented artifacts are handled more explicitly.
4.1.6 note:
- Screenshot path resolution receives project references in
ToMatchScreenshotResolvePath, which matters in multi-project browser suites that store snapshots per project/browser.
Mixed Node + Browser Projects
export default defineConfig({
test: {
projects: [
{
test: {
name: "unit",
include: ["tests/unit/**/*.test.ts"],
environment: "node",
},
},
{
test: {
name: "browser",
include: ["tests/browser/**/*.test.ts"],
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: "chromium" }],
},
},
},
],
},
});Available Browsers
Playwright:
chromiumfirefoxwebkit
WebdriverIO:
chromefirefoxedgesafari
Writing Browser Tests
Basic Test
import { expect, test } from "vitest";
import { page } from "vitest/browser";
test("renders content", async () => {
document.body.innerHTML = "<div>Hello World</div>";
await expect.element(page.getByText("Hello World")).toBeInTheDocument();
});With Locators
import { page, userEvent } from "vitest/browser";
test("form interaction", async () => {
// Find elements
const input = page.getByLabelText(/username/i);
const button = page.getByRole("button", { name: /submit/i });
// Interact
await input.fill("john");
await button.click();
// Assert
await expect.element(page.getByText("Welcome, john")).toBeVisible();
});Locator Methods
import { page } from "vitest/browser";
// By role (recommended)
page.getByRole("button", { name: "Submit" });
page.getByRole("textbox", { name: /email/i });
page.getByRole("heading", { level: 1 });
// By text
page.getByText("Hello World");
page.getByText(/hello/i); // regex
// By label
page.getByLabelText("Username");
// By placeholder
page.getByPlaceholder("Enter email");
// By test ID
page.getByTestId("submit-button");
// By title
page.getByTitle("Close dialog");
// By alt text
page.getByAltText("Profile picture");
// CSS selector (escape hatch)
page.elementLocator(document.querySelector(".my-class"));User Events
import { userEvent, page } from "vitest/browser";
// Typing
await userEvent.type(input, "Hello");
await input.fill("Hello"); // Alternative
// Clicking
await userEvent.click(button);
await button.click(); // Alternative
// Keyboard
await userEvent.keyboard("{Enter}");
// Hover
await userEvent.hover(element);
// Focus
await userEvent.focus(input);
// Select
await userEvent.selectOptions(select, ["option1"]);
// File upload
await userEvent.upload(fileInput, file);
// Drag and drop
await userEvent.dragAndDrop(source, target);
// Wheel / scroll (v4.1.0)
await userEvent.wheel(element, { deltaY: 120 });Browser Assertions
import { expect } from "vitest";
import { page } from "vitest/browser";
// Element exists
await expect.element(page.getByText("Hello")).toBeInTheDocument();
// Visibility
await expect.element(locator).toBeVisible();
await expect.element(locator).not.toBeVisible();
// Enabled/Disabled
await expect.element(button).toBeEnabled();
await expect.element(button).toBeDisabled();
// Value
await expect.element(input).toHaveValue("text");
// Text content
await expect.element(heading).toHaveTextContent("Title");
// Attribute
await expect.element(link).toHaveAttribute("href", "/home");
// Class
await expect.element(element).toHaveClass("active");
// Focus
await expect.element(input).toBeFocused();Component Testing
React
npm install -D vitest-browser-reactimport { render } from "vitest-browser-react";
import { expect, test } from "vitest";
import Button from "./Button";
test("button click", async () => {
const screen = render(<Button>Click me</Button>);
await screen.getByRole("button").click();
await expect.element(screen.getByText("Clicked!")).toBeVisible();
});Vue
npm install -D vitest-browser-vueimport { render } from "vitest-browser-vue";
import Component from "./Component.vue";
test("v-model works", async () => {
const screen = render(Component);
await screen.getByLabelText(/username/i).fill("Bob");
await expect.element(screen.getByText("Hi, Bob")).toBeInTheDocument();
});Svelte
npm install -D vitest-browser-svelteimport { render } from "vitest-browser-svelte";
import Counter from "./Counter.svelte";
test("counter increments", async () => {
const screen = render(Counter);
await screen.getByRole("button").click();
await expect.element(screen.getByText("1")).toBeVisible();
});Visual Regression Testing
import { page } from "vitest/browser";
test("visual snapshot", async () => {
// Full page screenshot
await expect(page.screenshot()).toMatchImageSnapshot();
// Element screenshot
const element = page.getByTestId("card");
await expect(element.screenshot()).toMatchImageSnapshot();
});Viewport Control
import { page } from "vitest/browser";
test("responsive design", async () => {
// Set viewport
await page.viewport(375, 667); // iPhone SE
await expect.element(page.getByTestId("mobile-menu")).toBeVisible();
await page.viewport(1920, 1080); // Desktop
await expect.element(page.getByTestId("desktop-nav")).toBeVisible();
});Running Browser Tests
# Run with browser mode
vitest --browser.enabled
# Specific browser
vitest --browser.name=chromium
# Headless
vitest --browser.headless
# With UI
vitest --browser.uiLimitations
No vi.spyOn on Imports
// ❌ Doesn't work in browser mode
import * as module from "./module";
vi.spyOn(module, "method");
// ✅ Use vi.mock with spy option
vi.mock("./module", { spy: true });Blocking Dialogs Mocked
alert(), confirm(), prompt() are automatically mocked because they block execution. Mock them explicitly for predictable behavior.
Best Practices
1. Use Playwright provider for CI - supports parallel execution 2. Use locators by role - most resilient selectors 3. Use `expect.element()` - waits for element automatically 4. Avoid `testing-library/user-event` - use vitest/browser instead 5. Use headless mode in CI 6. Separate browser tests into their own project
Vitest CLI Reference
Command line interface for running tests.
Basic Commands
# Run tests in watch mode (default in dev)
vitest
# Run tests once (no watch)
vitest run
# Watch mode (explicit)
vitest watch
# Run benchmarks
vitest bench
# Run type checking
vitest typecheck
# List matching tests without running
vitest list
vitest list --filesOnly
# Initialize browser testing
vitest init browserv4.1.0 additions
vitest listcan statically collect tests instead of executing files to discover them.--detect-async-leakshelps surface leaked async work that makes runs flaky or hang.--updatenow acceptsnew,all, andnonefor finer snapshot control.
Test Filtering
# Filter by file path (contains "foobar")
vitest foobar
# Filter by filename and line number
vitest src/utils.test.ts:10
vitest ./src/utils.test.ts:10
vitest /absolute/path/utils.test.ts:10
# Filter by test name pattern
vitest -t "should work"
vitest --testNamePattern="user.*login"
# Exclude patterns
vitest --exclude "**/*.e2e.test.ts"
# Run related tests (for lint-staged)
vitest related src/index.ts src/utils.ts --run
# Run only changed files
vitest --changed
vitest --changed HEAD~1
vitest --changed origin/mainCoverage
# Enable coverage
vitest --coverage
vitest run --coverage
# With options
vitest --coverage.enabled --coverage.provider=istanbul
vitest --coverage.reporter=html --coverage.reporter=json
# Thresholds
vitest --coverage.thresholds.100
vitest --coverage.thresholds.lines=80
vitest --coverage.thresholds.functions=80
vitest --coverage.thresholds.branches=80
vitest --coverage.thresholds.statements=80
# Output directory
vitest --coverage.reportsDirectory=./reportsEnvironment
# Set environment
vitest --environment=jsdom
vitest --environment=happy-dom
vitest --environment=node
# Set pool
vitest --pool=threads
vitest --pool=forks
vitest --pool=vmThreads
# Enable globals
vitest --globalsWatch Mode Options
# Disable watch (same as "run")
vitest --run
# Start without running tests (run on change)
vitest --standalone
# Clear screen on rerun
vitest --clearScreenReporters
# Use specific reporter
vitest --reporter=verbose
vitest --reporter=dot
vitest --reporter=json
vitest --reporter=junit
vitest --reporter=html
vitest --reporter=github-actions
# Multiple reporters
vitest --reporter=default --reporter=json
# Output to file
vitest --outputFile=./results.json
vitest --outputFile.json=./json.json --outputFile.junit=./junit.xml4.1.4 note:
- If you need to filter or sanitize test
metain JSON output, configure the JSON reporter withfilterMetainvitest.config.*; do not assume the CLI shorthand alone is enough for reporter-specific options.
UI
# Open Vitest UI
vitest --ui
# Auto-open in browser
vitest --ui --open
# Specify port
vitest --api.port=51204Debugging
# Enable Node.js inspector
vitest --inspect
vitest --inspect=127.0.0.1:9229
# Break before test starts
vitest --inspectBrkTimeouts
# Test timeout (default: 5000ms)
vitest --testTimeout=10000
# Hook timeout (default: 10000ms)
vitest --hookTimeout=30000
# Teardown timeout
vitest --teardownTimeout=10000Snapshot update control (v4.1.0)
vitest -u --update=new
vitest -u --update=all
vitest -u --update=noneParallelism
# Max worker threads
vitest --maxWorkers=4
# Disable file parallelism
vitest --no-file-parallelism
# Disable isolation
vitest --no-isolate
# Max concurrent tests
vitest --maxConcurrency=10Retry & Bail
# Retry failed tests
vitest --retry=3
# Stop after N failures
vitest --bail=1Sharding (CI)
# Split tests into 3 parts
vitest run --shard=1/3
vitest run --shard=2/3
vitest run --shard=3/3
# Merge reports from shards
vitest --merge-reports --reporter=junitBrowser Mode
# Enable browser testing
vitest --browser.enabled
vitest --browser.name=chromium
vitest --browser.name=firefox
vitest --browser.name=webkit
# Headless mode
vitest --browser.headless
# Browser UI
vitest --browser.uiType Checking
# Enable type checking
vitest --typecheck.enabled
# Run only type tests
vitest typecheck
vitest --typecheck.only
# Specify checker
vitest --typecheck.checker=tsc
vitest --typecheck.checker=vue-tscProject Selection
# Run specific project
vitest --project=unit
vitest --project=e2e
# Multiple projects
vitest --project=unit --project=integration
# Wildcard
vitest --project="packages*"
# Exclude
vitest --project="!e2e"Output Control
# Silent mode
vitest --silent
# Only show failed test logs
vitest --silent=passed-only
# Hide skipped tests
vitest --hideSkippedTests
# Disable colors
vitest --no-color
# Log heap usage
vitest --logHeapUsageSnapshots
# Update snapshots
vitest -u
vitest --update
# Expand snapshot diff
vitest --expandSnapshotDiffSequences
# Shuffle tests
vitest --sequence.shuffle.tests
vitest --sequence.shuffle.files
# Set shuffle seed
vitest --sequence.seed=12345
# Run concurrently
vitest --sequence.concurrentConfiguration
# Use specific config
vitest --config=./vitest.e2e.config.ts
vitest -c ./custom-config.ts
# Set root directory
vitest --root=./packages/core
vitest -r ./packages/core
# Set test directory
vitest --dir=./tests
# Clear cache
vitest --clearCacheCommon Patterns
CI Pipeline
# Basic CI run
vitest run --coverage --reporter=junit --outputFile=./junit.xml
# With coverage thresholds
vitest run --coverage --coverage.thresholds.100lint-staged Integration
// .lintstagedrc.js
export default {
"*.{js,ts}": "vitest related --run",
};Watch Specific Files
vitest --watch src/utilsDebug Single Test
vitest --inspect-brk src/specific.test.tsExit Codes
0- All tests passed1- Tests failed or errors occurred- Non-zero with
--bailif threshold exceeded
Vitest Configuration Reference
Complete configuration options for vitest.config.ts or vite.config.ts.
Config File Setup
Standalone vitest.config.ts (Recommended)
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// test options here
},
});Using vite.config.ts
/// <reference types="vitest/config" />
import { defineConfig } from "vite";
export default defineConfig({
test: {
// test options here
},
});Extending Vite Config
import { defineConfig, mergeConfig } from "vitest/config";
import viteConfig from "./vite.config";
export default mergeConfig(
viteConfig,
defineConfig({
test: {
exclude: ["packages/template/*"],
},
}),
);Using Defaults
import { configDefaults, defineConfig } from "vitest/config";
export default defineConfig({
test: {
exclude: [...configDefaults.exclude, "packages/template/*"],
},
});Essential Options
Test File Patterns
{
// Files to include (glob patterns)
include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'],
// Files to exclude
exclude: ['**/node_modules/**', '**/.git/**'],
// In-source testing
includeSource: ['src/**/*.{js,ts}'],
}Globals
{
// Enable global test APIs (test, expect, describe, etc.)
globals: true,
}
// Add to tsconfig.json for TypeScript:
// { "compilerOptions": { "types": ["vitest/globals"] } }Environment
{
// 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' | string
environment: 'jsdom',
// Environment options
environmentOptions: {
jsdom: {
url: 'http://localhost:3000',
},
},
}
// Per-file environment (docblock at top of file):
// /** @vitest-environment jsdom */Pool (Test Runner)
{
// 'threads' | 'forks' | 'vmThreads' | 'vmForks'
pool: 'forks', // default
// Pool-specific options
poolOptions: {
threads: {
singleThread: true,
},
forks: {
singleFork: true,
},
},
}Pool Types:
threads- Worker threads (fast, but can't useprocess.chdir())forks- Child process (default, supports process APIs)vmThreads- VM context in threads (fastest, but unstable ESM)vmForks- VM context in forks
Timeouts
{
testTimeout: 5000, // Per-test timeout (default: 5000ms)
hookTimeout: 10000, // Setup/teardown timeout (default: 10000ms)
teardownTimeout: 10000, // Global teardown timeout
}Setup Files
{
// Run before each test file
setupFiles: ['./test/setup.ts'],
// Run once before all tests
globalSetup: ['./test/global-setup.ts'],
}Mock Configuration
{
clearMocks: true, // Clear mock calls before each test
mockReset: true, // Reset mock implementations before each test
restoreMocks: true, // Restore original implementations before each test
unstubEnvs: true, // Restore env vars after each test
unstubGlobals: true, // Restore globals after each test
}Coverage
{
coverage: {
enabled: true,
provider: 'v8', // 'v8' | 'istanbul'
instrumenter: 'v8', // Optional in newer 4.1.x coverage flows when instrumenter choice matters
reporter: ['text', 'json', 'html'],
reportsDirectory: './coverage',
// Files to include/exclude
include: ['src/**/*.ts'],
exclude: ['**/*.test.ts', '**/*.d.ts'],
// Thresholds (fail if below)
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
perFile: true, // Check per file
autoUpdate: true, // Auto-update thresholds
100: false, // Require 100% coverage
},
// Skip files with 100% coverage in report
skipFull: false,
// Generate report even if tests fail
reportOnFailure: true,
},
}4.1.5 note:
- Coverage now exposes an
instrumenteroption in the4.1.xline. Keep it explicit if your coverage pipeline depends on a specific transformation path instead of accepting the default provider behavior.
v4.1.0 notes
- Snapshot update policy can now be expressed more explicitly (
new,all,none) instead of a simple binary update mode. - If you expose Vitest through the programmatic API, additional
apipermissions likeallowWriteandallowExecare relevant when static collection/execution is delegated.
Reporters
{
// Built-in reporters
reporters: ['default'],
// Options: 'basic', 'default', 'verbose', 'dot', 'json', 'html',
// 'junit', 'github-actions', 'blob', 'tap', 'tap-flat'
// With options
reporters: [
['json', { outputFile: './test-results.json' }],
['junit', { outputFile: './junit.xml' }],
],
// Output file shorthand
outputFile: {
json: './test-results.json',
junit: './junit.xml',
},
}Reporter note for 4.1.4:
- The JSON reporter now supports
filterMeta, which lets you control how testmetais emitted in machine-readable output. - If your CI/report consumer depends on
meta, pin the expected shape explicitly instead of assuming every key will always be present.
Watch Mode
{
watch: true, // Enable watch mode (default in dev)
// Patterns that force full rerun
forceRerunTriggers: ['**/package.json', '**/*.config.*'],
// Watch-specific patterns
watchTriggerPatterns: ['src/**', 'test/**'],
}4.1.6 note:
- If you rely on
sequence.concurrent, re-test any local workarounds:4.1.6fixes concurrent sequencing behavior that previously produced inconsistent scheduling.
Test Filtering
{
// Filter by test name pattern
testNamePattern: /should.*work/,
// Allow test.only in CI
allowOnly: false, // default: !process.env.CI
// Pass with no test files
passWithNoTests: false,
}Parallelism
{
// Run test files in parallel
fileParallelism: true,
// Max concurrent test files
maxWorkers: 4,
// Max concurrent tests within a file
maxConcurrency: 5,
// Isolate test files
isolate: true,
}Sequences
{
sequence: {
// Hook execution order: 'stack' | 'list' | 'parallel'
hooks: 'parallel',
// Shuffle tests
shuffle: false,
seed: 123, // Shuffle seed
},
}Snapshots
{
// Snapshot format options
snapshotFormat: {
printBasicPrototype: false,
},
// Custom serializers
snapshotSerializers: ['./custom-serializer.ts'],
// Custom snapshot path resolver
resolveSnapshotPath: (path, ext) => path.replace('src', '__snapshots__') + ext,
}Retry & Bail
{
retry: 2, // Retry failed tests N times
bail: 1, // Stop after N failures (0 = no bail)
}Typecheck
{
typecheck: {
enabled: true,
checker: 'tsc', // 'tsc' | 'vue-tsc'
include: ['**/*.{test,spec}-d.?(c|m)[jt]s?(x)'],
},
}Fake Timers
{
fakeTimers: {
// Which APIs to mock
toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date'],
// Loop limit for runAllTimers
loopLimit: 10000,
},
}Benchmarks
{
benchmark: {
include: ['**/*.{bench,benchmark}.?(c|m)[jt]s?(x)'],
exclude: ['**/node_modules/**'],
outputFile: './bench/results.json',
reporters: ['default'],
},
}UI
{
ui: true, // Enable Vitest UI
open: true, // Auto-open in browser
api: {
port: 51204,
strictPort: true,
},
}Multi-Project Configuration
{
projects: [
{
name: 'unit',
include: ['test/unit/**/*.test.ts'],
environment: 'node',
},
{
name: 'browser',
include: ['test/browser/**/*.test.ts'],
browser: {
enabled: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }],
},
},
],
}Browser Mode
{
browser: {
enabled: true,
provider: 'playwright', // 'playwright' | 'webdriverio' | 'preview'
instances: [
{ browser: 'chromium' },
{ browser: 'firefox' },
{ browser: 'webkit' },
],
headless: true,
viewport: { width: 1280, height: 720 },
// Screenshot on failure
screenshotFailures: true,
screenshotDirectory: './screenshots',
},
}CLI Options
# Run tests
vitest
vitest run # Run once (no watch)
vitest watch # Watch mode
# Filtering
vitest src/utils # Run tests in path
vitest --testNamePattern="should work"
vitest --exclude "**/*.integration.test.ts"
# Coverage
vitest --coverage
vitest --coverage.enabled --coverage.provider=istanbul
# Environment
vitest --environment jsdom
vitest --pool threads
# Other
vitest --ui # Open UI
vitest --reporter=json # Change reporter
vitest bench # Run benchmarks
vitest typecheck # Run type checking
vitest --bail 1 # Stop on first failure
vitest --retry 2 # Retry failed testsOpenTelemetry (Experimental)
Enable distributed tracing for test execution. Requires @opentelemetry/sdk-node.
npm install @opentelemetry/sdk-node// otel-setup.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
export async function setup() {
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: "http://localhost:4318/v1/traces",
}),
});
sdk.start();
return async () => await sdk.shutdown();
}// vitest.config.ts
export default defineConfig({
test: {
experimental: {
openTelemetry: {
enabled: true,
sdkPath: "./otel-setup.ts",
},
},
},
});Browser Mode OpenTelemetry
{
browser: {
enabled: true,
// ...
},
experimental: {
openTelemetry: {
enabled: true,
sdkPath: './otel-setup-node.ts', // Node.js SDK
browserSdkPath: './otel-setup-browser.ts', // Browser SDK
},
},
}CI/CD Context Propagation
Pass trace context via environment variables:
TRACEPARENT="00-<trace-id>-<span-id>-01" vitest run
TRACESTATE="key=value" vitest runThis links test spans to parent CI pipeline traces.
Config Hierarchy
1. CLI flags (highest priority) 2. vitest.config.ts 3. vite.config.ts 4. Defaults
Note: Configuration options marked with 🚫 in docs can only be set at root level, not in project configs.
Vitest Expect API Reference
Assertion matchers and utilities.
Basic Matchers
import { expect } from "vitest";
// Identity (Object.is)
expect(value).toBe(expected);
expect(obj).not.toBe(otherObj); // Different references
// Deep equality
expect(obj).toEqual({ a: 1, b: { c: 2 } });
// Strict equality (respects types and object properties)
expect({ a: undefined }).not.toStrictEqual({}); // undefined !== missing
expect(new MyClass()).not.toStrictEqual({ prop: true }); // class !== plain object
// Type checks
expect(value).toBeDefined();
expect(value).toBeUndefined();
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeNaN();
expect(value).toBeTypeOf("number"); // typeof check
expect(obj).toBeInstanceOf(MyClass);Number Matchers
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(3);
expect(value).toBeLessThanOrEqual(3);
// Floating point (avoids rounding errors)
expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // numDigits default: 5String Matchers
expect("hello world").toContain("world");
expect("hello").toMatch(/^hel/);
expect("hello").toMatch("ell");Array/Object Matchers
// Contains item
expect(["a", "b", "c"]).toContain("b");
expect("hello").toContain("ell");
// Contains item with deep equality
expect([{ a: 1 }]).toContainEqual({ a: 1 });
// Has length
expect([1, 2, 3]).toHaveLength(3);
expect("hello").toHaveLength(5);
// Has property
expect(obj).toHaveProperty("a.b.c");
expect(obj).toHaveProperty("a.b", "value");
expect(obj).toHaveProperty(["a", "b"], "value"); // key array
// Partial object match
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 });
expect([{ a: 1 }, { b: 2 }]).toMatchObject([{ a: 1 }, { b: 2 }]);Error Matchers
// Throws error
expect(() => throwFn()).toThrow();
expect(() => throwFn()).toThrow("message");
expect(() => throwFn()).toThrow(/regex/);
expect(() => throwFn()).toThrow(ErrorClass);
expect(() => throwFn()).toThrowError(expected); // alias for toThrowSnapshot Matchers
// External snapshot file
expect(data).toMatchSnapshot();
expect(data).toMatchSnapshot("custom hint");
expect(data).toMatchSnapshot({ optional: true }); // optional shape
// Inline snapshot (auto-updates)
expect(data).toMatchInlineSnapshot();
expect(data).toMatchInlineSnapshot(`
{
"foo": "bar"
}
`);
// File snapshot (saves to separate file)
expect(data).toMatchFileSnapshot("./output.txt");Snapshot notes (v4.1.4)
- Vitest
4.1.4adds experimental ARIA snapshot support. - Treat this as an evolving surface: check the current upstream docs/examples before baking helpers or custom abstractions around it.
Mock Matchers
import { vi, expect } from "vitest";
const mock = vi.fn();
// Called
expect(mock).toHaveBeenCalled();
expect(mock).toHaveBeenCalledTimes(2);
// Arguments
expect(mock).toHaveBeenCalledWith("arg1", "arg2");
expect(mock).toHaveBeenLastCalledWith("arg");
expect(mock).toHaveBeenNthCalledWith(2, "arg"); // 2nd call
// Return values
expect(mock).toHaveReturned();
expect(mock).toHaveReturnedTimes(2);
expect(mock).toHaveReturnedWith("value");
expect(mock).toHaveLastReturnedWith("value");
expect(mock).toHaveNthReturnedWith(2, "value");Async Matchers
// Promise resolution
await expect(Promise.resolve("ok")).resolves.toBe("ok");
await expect(Promise.resolve(obj)).resolves.toEqual({ a: 1 });
// Promise rejection
await expect(Promise.reject("error")).rejects.toBe("error");
await expect(Promise.reject(new Error())).rejects.toThrow();
// Polling (retries until passes or times out)
await expect.poll(() => fetchStatus()).toBe("ready");
await expect
.poll(() => result, {
interval: 50, // check interval (default: 50ms)
timeout: 5000, // max wait (default: 1000ms)
message: "custom error message",
})
.toBe("expected");Soft Assertions
// Continue test even if assertion fails
expect.soft(value).toBe(1);
expect.soft(other).toBe(2);
// Test fails at end if any soft assertion failedAssertion Counting
test("has correct assertions", () => {
expect.assertions(2); // exactly 2 assertions must run
expect.hasAssertions(); // at least 1 assertion must run
// Unreachable (utility)
if (condition) {
expect.unreachable("should not reach here");
}
});Custom Matchers
import { expect } from "vitest";
expect.extend({
toBeEven(received) {
const pass = received % 2 === 0;
return {
pass,
message: () => (pass ? `expected ${received} not to be even` : `expected ${received} to be even`),
};
},
});
// TypeScript
interface CustomMatchers<R = unknown> {
toBeEven(): R;
}
declare module "vitest" {
interface Assertion<T> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
// Usage
expect(4).toBeEven();
expect(3).not.toBeEven();Asymmetric Matchers
// Match anything (not null/undefined)
expect(fn).toHaveBeenCalledWith(expect.anything());
// Match by type
expect(fn).toHaveBeenCalledWith(expect.any(Number));
expect(fn).toHaveBeenCalledWith(expect.any(String));
// Partial array
expect(arr).toEqual(expect.arrayContaining([1, 2]));
// Partial object
expect(obj).toEqual(expect.objectContaining({ key: "value" }));
// String patterns
expect(obj).toEqual({
name: expect.stringContaining("John"),
email: expect.stringMatching(/@/),
});
// Negation
expect(arr).toEqual(expect.not.arrayContaining([4]));
expect(str).not.toEqual(expect.stringContaining("foo"));
// Closeness for numbers
expect(arr).toEqual([expect.closeTo(10.1, 1)]);Modifier: not
// Negate any matcher
expect(value).not.toBe(other);
expect(arr).not.toContain(item);
expect(fn).not.toHaveBeenCalled();Important Notes
- Floating point: Use
toBeCloseTofor float comparisons - Reference equality:
toBeusesObject.is, not=== - Undefined vs missing:
toStrictEqualdistinguishes them - Concurrent tests: Use
expectfrom context for reliable snapshots - Mock assertions: Only work with spy functions from
vi.fn()orvi.spyOn()
Vitest Mocking Reference
Comprehensive guide to mocking functions, modules, timers, and globals.
Creating Mocks
vi.fn() - Mock Function
import { vi, expect } from "vitest";
// Empty mock (returns undefined)
const mock = vi.fn();
// With implementation
const mockFn = vi.fn((x: number) => x + 1);
// Assertions
mockFn(5);
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(5);
expect(mockFn).toHaveReturnedWith(6);vi.spyOn() - Spy on Method
const cart = {
getApples: () => 42,
};
const spy = vi.spyOn(cart, "getApples");
cart.getApples();
expect(spy).toHaveBeenCalled();
expect(spy).toHaveReturnedWith(42);
// With replacement
spy.mockImplementation(() => 100);
expect(cart.getApples()).toBe(100);
// Restore original
spy.mockRestore();vi.mockObject() (v3.2.0+)
const original = {
simple: () => "value",
nested: { method: () => "real" },
prop: "foo",
};
const mocked = vi.mockObject(original);
mocked.simple.mockReturnValue("mocked");
// With spy mode (keep implementations)
const spied = vi.mockObject(original, { spy: true });Mock Properties
const mock = vi.fn();
mock("arg1", "arg2");
mock("arg3");
// All calls
mock.mock.calls; // [['arg1', 'arg2'], ['arg3']]
mock.mock.lastCall; // ['arg3']
mock.mock.results; // [{ type: 'return', value: undefined }, ...]
mock.mock.settledResults; // For async - { type: 'fulfilled'/'rejected', value }
mock.mock.instances; // Instances when called with `new`
mock.mock.contexts; // `this` values
mock.mock.invocationCallOrder; // [1, 2, ...]Mock Return Values
const mock = vi.fn();
// Always return value
mock.mockReturnValue(42);
// Return value once (chainable)
mock.mockReturnValueOnce("first").mockReturnValueOnce("second").mockReturnValue("default");
// For promises
mock.mockResolvedValue({ data: "ok" });
mock.mockResolvedValueOnce({ data: "first" });
mock.mockRejectedValue(new Error("fail"));
mock.mockRejectedValueOnce(new Error("once"));
// Return this
mock.mockReturnThis();Mock Implementations
const mock = vi.fn();
// Permanent implementation
mock.mockImplementation((x) => x * 2);
// One-time implementations
mock.mockImplementationOnce(() => "first").mockImplementationOnce(() => "second");
// Temporary implementation
mock.withImplementation(
() => "temp",
() => {
mock(); // 'temp'
},
);
mock(); // back to originalReset/Clear/Restore
// Clear call history only
mock.mockClear();
// Clear history + reset implementation
mock.mockReset();
// Clear + reset + restore original (for spies)
mock.mockRestore();
// Global versions
vi.clearAllMocks(); // clearMocks config
vi.resetAllMocks(); // mockReset config
vi.restoreAllMocks(); // restoreMocks configModule Mocking
vi.mock() - Hoisted
import { myFunc } from "./module";
// Automock (returns undefined for all exports)
vi.mock("./module");
// Factory (hoisted to top)
vi.mock("./module", () => ({
myFunc: vi.fn(() => "mocked"),
default: { key: "value" }, // for default export
}));
// With spy mode (keeps implementation)
vi.mock("./module", { spy: true });
// Access original inside factory
vi.mock("./module", async (importOriginal) => {
const mod = await importOriginal();
return { ...mod, myFunc: vi.fn() };
});vi.doMock() - Not Hoisted
// For dynamic imports (not hoisted)
vi.doMock("./module", () => ({ myFunc: () => "mocked" }));
const { myFunc } = await import("./module");As of v4.1.0, doMock() can return a disposable cleanup handle. Use it when you want an explicit scoped mock lifecycle.
vi.unmock() / vi.doUnmock()
vi.unmock("./module"); // Hoisted
vi.doUnmock("./module"); // Not hoistedvi.hoisted() - Define Variables Before Imports
// Variables defined in vi.hoisted are available in vi.mock
const mocks = vi.hoisted(() => ({
myFunc: vi.fn(),
}));
vi.mock("./module", () => ({
myFunc: mocks.myFunc,
}));
mocks.myFunc.mockReturnValue(100);Helper Functions
// Import original (bypass mock)
const original = await vi.importActual("./module");
// Import with auto-mock
const mocked = await vi.importMock("./module");
// Type helper
vi.mocked(myFunc).mockReturnValue("typed");
vi.mocked(myFunc, { deep: true }); // Deep mock types
// Check if mocked
vi.isMockFunction(myFunc); // booleanmocks Folder
project/
├── __mocks__/
│ └── axios.js # Mock for node_modules
├── src/
│ ├── __mocks__/
│ │ └── utils.js # Mock for ./utils
│ └── utils.js// Auto-uses __mocks__/axios.js
vi.mock("axios");Fake Timers
Enable/Disable
// Enable fake timers
vi.useFakeTimers();
// Restore real timers
vi.useRealTimers();
// Check if fake timers active
### Timer controls in v4.1.0
- `setTickMode` is exposed via fake-timer controls after the upgrade to sinon/fake-timers v15.
- Use it when advancing timers must coordinate more predictably with queued async work.
### Throwing mocks in v4.1.0
- `mockThrow` and `mockThrowOnce` simplify exception-oriented mocks.
- Prefer them when the intent is "this mock throws" instead of wrapping each case in `mockImplementation(() => { throw ... })`.
vi.isFakeTimers(); // booleanAdvance Time
vi.useFakeTimers();
setTimeout(() => console.log("done"), 1000);
// Advance by milliseconds
vi.advanceTimersByTime(1000);
// Advance to next timer
vi.advanceTimersToNextTimer();
// Run all timers
vi.runAllTimers();
// Run only pending (not new ones)
vi.runOnlyPendingTimers();
// For requestAnimationFrame
vi.advanceTimersToNextFrame();
// Async versions (for async callbacks)
await vi.advanceTimersByTimeAsync(1000);
await vi.runAllTimersAsync();System Time
vi.useFakeTimers();
// Set system time
vi.setSystemTime(new Date(2024, 0, 1));
expect(Date.now()).toBe(new Date(2024, 0, 1).valueOf());
// Get mocked time
vi.getMockedSystemTime(); // Date | null
// Get real time even with fake timers
vi.getRealSystemTime(); // numberTimer Utilities
// Count pending timers
vi.getTimerCount();
// Clear all scheduled timers
vi.clearAllTimers();
// Run all microtasks (process.nextTick)
vi.runAllTicks();Environment & Globals
vi.stubEnv()
vi.stubEnv("NODE_ENV", "production");
process.env.NODE_ENV === "production";
import.meta.env.NODE_ENV === "production";
// Restore all
vi.unstubAllEnvs();vi.stubGlobal()
vi.stubGlobal("innerWidth", 1024);
vi.stubGlobal("IntersectionObserver", MockObserver);
// Restore all
vi.unstubAllGlobals();Utilities
vi.waitFor() - Poll Until Success
await vi.waitFor(
() => {
if (!server.isReady) throw new Error("Not ready");
},
{ timeout: 5000, interval: 100 },
);vi.waitUntil() - Poll Until Truthy
const element = await vi.waitUntil(() => document.querySelector(".loaded"), { timeout: 5000 });vi.dynamicImportSettled()
// Wait for all dynamic imports to resolve
function renderComponent() {
import("./component").then(({ render }) => render());
}
renderComponent();
await vi.dynamicImportSettled();vi.setConfig() / vi.resetConfig()
vi.setConfig({
testTimeout: 10000,
clearMocks: true,
fakeTimers: { now: new Date(2024, 0, 1) },
});
vi.resetConfig(); // Restore originalCommon Patterns
Auto-restore Mocks
// vitest.config.ts
export default {
test: {
clearMocks: true, // mockClear before each
mockReset: true, // mockReset before each
restoreMocks: true, // mockRestore before each
unstubEnvs: true, // unstubAllEnvs after each
unstubGlobals: true, // unstubAllGlobals after each
},
};Using Statement (Auto-cleanup)
// With explicit resource management
it("test", () => {
using spy = vi.spyOn(console, "log");
// spy.mockRestore() called automatically at block end
});Module Reset Between Tests
beforeEach(() => {
vi.resetModules();
});
test("test", async () => {
const { state } = await import("./module");
// Fresh module instance
});