
Vitest
- 3 installs
- 19 repo stars
- Updated August 1, 2026
- xobotyi/cc-foundry
Helps with testing & qa tasks during AI-assisted development.
About
vitest is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted coding.
- vitest
- Testing & QA
- AI-coding skill
Vitest by the numbers
- 3 all-time installs (skills.sh)
- Ranked #1,649 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/xobotyi/cc-foundry --skill vitestAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | xobotyi/cc-foundry ↗ |
What it does
Helps with testing & qa tasks during AI-assisted development.
Files
Vitest
Test behavior, not implementation. Mock boundaries, not internals.
Vitest is a Vite-native test framework with Jest-compatible APIs. It shares your app's Vite config (aliases, plugins, transforms) so tests run against the same code you ship.
References
| Topic | Reference | Contents |
|---|---|---|
| Mocking | [${CLAUDE_SKILL_DIR}/references/mocking.md] | Full mocking rules, module mocking patterns, cleanup strategy |
| Assertions | [${CLAUDE_SKILL_DIR}/references/assertions.md] | Matcher tables, asymmetric matchers, soft assertions |
| Lifecycle | [${CLAUDE_SKILL_DIR}/references/lifecycle.md] | Hook execution order, test context, setup files, global setup |
| Configuration | [${CLAUDE_SKILL_DIR}/references/configuration.md] | Config file options, projects, pools, sharding, env vars |
| Coverage | [${CLAUDE_SKILL_DIR}/references/coverage.md] | Coverage providers, thresholds, ignore comments, performance |
| Jest migration | [${CLAUDE_SKILL_DIR}/references/jest-migration.md] | Jest API translation, key behavioral differences, Mocha/Sinon |
Test Structure
- Import explicitly.
import { describe, it, expect, vi } from 'vitest'— do not
rely on globals unless the project has globals: true configured.
- One concept per test. Name tests by behavior: `'returns empty array when input
is null', not 'test case 1'`.
- Use `describe` for grouping. Group by unit (function, class, component), not by
test type.
- Prefer `it` over `test`. Both work, but
itreads better insidedescribe:
describe('parseUrl', () => { it('extracts hostname', ...) }).
- Use `it.each` / `describe.each` for parametrized tests. Supports array form and
template literal form with $key interpolation.
- Test modifiers:
it.skip,it.only,it.todo,it.fails,it.skipIf(cond),
it.runIf(cond).
- Retry:
it('name', { retry: 3 }, fn). Repeat:it('name', { repeats: 100 }, fn). - Concurrent tests:
describe.concurrent(...)— useexpectfrom test context
(destructured parameter) for correct snapshot/assertion tracking.
Mocking
Function Mocks
- `vi.fn()` creates a standalone trackable mock. Optionally accepts implementation.
- `vi.spyOn(obj, 'method')` wraps existing method while preserving original. Also
supports vi.spyOn(obj, 'prop', 'get') for getters/setters.
- Prefer `vi.spyOn` over `vi.mock` when you only need to observe or override a
single export.
Module Mocking
- `vi.mock()` is hoisted. It moves to top of file regardless of where you write it.
Always runs before imports.
- Factory must return an object with explicit exports. ESM requires explicit
default
key: vi.mock('./mod', () => ({ default: val, namedExport: vi.fn() })).
- Partial mocking with `importOriginal`:
vi.mock(import('./api'), async (importOriginal) => ({ ...await importOriginal(), fetchUser: vi.fn() })).
- `vi.doMock()` is not hoisted — runs at position. Only affects subsequent dynamic
import() calls. Use when you need per-test mock behavior.
- `vi.mock` cannot intercept internal calls. If
foo()callsbar()in the same
file, mocking bar externally does not affect foo. Refactor to separate modules or use dependency injection.
Cleanup & Timers
- Always restore mocks. Use
restoreMocks: truein config (recommended) or
afterEach(() => vi.restoreAllMocks()).
- Pair `vi.useFakeTimers()` with `vi.useRealTimers()` — in
beforeEach/afterEach
or use fakeTimers config option.
- `vi.mocked(fn)` narrows TypeScript types to mock types without runtime changes.
Full mocking rules (auto-mocking, spy mode, vi.hoisted, __mocks__ directory, env/globals stubbing, async helpers): see ${CLAUDE_SKILL_DIR}/references/mocking.md.
Assertions
Value Matchers
| Matcher | Use When |
|---|---|
toBe(val) | Primitives or same reference (Object.is) |
toEqual(val) | Deep structural equality (ignores undefined in expected) |
toStrictEqual(val) | Deep equality + checks undefined keys, sparse arrays, class types |
toMatchObject(subset) | Object contains at least these properties |
Core Rules
- Sync errors: wrap in function:
expect(() => throwingFn()).toThrow('message'). - Async errors:
await expect(asyncFn()).rejects.toThrow('message'). - Always `await` async assertions.
await expect(promise).resolves.toEqual(...)— an
un-awaited assertion silently passes.
- `expect.poll(() => value, { timeout, interval })` — retries assertion until pass or
timeout. Prefer over manual waitFor loops.
- `expect.soft(val)` continues after failure, reports all errors at end.
- Asymmetric matchers inside
toEqual,toHaveBeenCalledWith:expect.any(Number),
expect.arrayContaining([...]), expect.objectContaining({}), expect.stringMatching(/regex/). Negate with expect.not.*.
- `expect.assertions(n)` — exactly n assertions must run.
`expect.hasAssertions()` — at least one. Guard against missing assertions in async code.
Truthiness, number, string/array/object matchers, type checks, spy assertions, custom error messages, expect.unreachable: see ${CLAUDE_SKILL_DIR}/references/assertions.md.
Snapshots
- File snapshots:
expect(val).toMatchSnapshot()— writes to.snapfile. - Inline snapshots:
expect(val).toMatchInlineSnapshot(\"expected"\)— Vitest
auto-updates the string argument.
- Property matchers for volatile data:
toMatchSnapshot({ id: expect.any(String) }).
Never snapshot timestamps, random IDs, or other volatile data without matchers.
- Prefer inline snapshots for small values — easier to review.
- Avoid large snapshots — they become rubber-stamp reviews.
- Commit snapshot files. Review them in PRs like any other code.
- Update with `vitest -u` or press
uin watch mode.
Lifecycle
Core Rules
- `beforeAll`/`afterAll` run once per
describeblock (or per file at top level). - `beforeEach`/`afterEach` run before/after every test in current scope.
- Teardown via return value: if
beforeAllorbeforeEachreturns a function, it
runs as teardown. Vitest-specific, not in Jest. Be careful not to accidentally return values — wrap in braces: beforeEach(() => { setupFn() }).
- `onTestFinished(fn)` — register cleanup inside a test. Always runs regardless of
pass/fail.
- `onTestFailed(fn)` — runs only on failure. Useful for diagnostics.
Setup Files
- `setupFiles: ['./test/setup.ts']` — runs before each test file in the same process.
Use for global hooks, custom matchers, shared setup.
- `globalSetup: ['./test/global-setup.ts']` — runs once before any test workers.
Use for expensive one-time setup (database seeding, server startup). Return a function for teardown.
Hook execution order, test context, hook order config, provide/inject, and global vs setup file comparison: see ${CLAUDE_SKILL_DIR}/references/lifecycle.md.
Configuration
- Prefer `vitest.config.ts` with
defineConfigfrom'vitest/config'. Inherits Vite
plugins and aliases automatically.
- If using `vite.config.ts`, add
/// <reference types="vitest/config" />directive. - Use `projects` (v3.2+, replaces deprecated
workspace) for multi-environment setups.
Every project must have a unique name.
Config file merging, key options table, pools and parallelism, environment variables, in-source testing, and sharding: see ${CLAUDE_SKILL_DIR}/references/configuration.md.
Coverage
Set coverage.include to catch uncovered files. Use v8 provider (default, recommended). Set thresholds in config. Run coverage only in CI, not in watch mode.
Provider comparison, reporters, ignore comments, and performance tips: see ${CLAUDE_SKILL_DIR}/references/coverage.md.
Extending Matchers
Define via expect.extend({ matcherName(received, ...args) {} }). Return { pass, message }. Add TypeScript declarations via interface Matchers<T> in vitest.d.ts.
Matcher context, TypeScript setup, and diff output: see ${CLAUDE_SKILL_DIR}/references/assertions.md.
Jest Migration
Replace jest.* with vi.*. Key differences: mock factory must return object with explicit exports, mockReset restores original impl, auto-mocking requires explicit vi.mock() call, hook return values are teardown functions.
Full API translation table, behavioral differences, and Mocha/Sinon migration: see ${CLAUDE_SKILL_DIR}/references/jest-migration.md.
Application
When writing tests:
- Apply all conventions silently — don't narrate each rule being followed.
- Match the project's existing test style (naming, structure, assertion library).
- If an existing codebase contradicts a convention, follow the codebase and flag the
divergence once.
When reviewing tests:
- Cite the specific issue and show the fix inline.
- Don't lecture — state what's wrong and how to fix it.
Integration
The javascript skill governs language choices; this skill governs Vitest testing decisions. Activate the relevant runtime skill (nodejs or bun) for runtime-specific behavior.
Test behavior, not implementation. When in doubt, mock less.
{
"sources": {
"Vitest LLMs Full Documentation": "https://vitest.dev/llms-full.txt",
"Configuration Guide": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/config/index.md",
"Test API Reference": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/api/expect.md",
"Vi Utility API (vi.fn, vi.mock, vi.spyOn, timers)": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/api/vi.md",
"Mock API Reference": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/api/mock.md",
"Mocking Guide (overview + cheat sheet)": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/mocking.md",
"Mocking Functions": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/mocking/functions.md",
"Mocking Modules": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/mocking/modules.md",
"Snapshot Testing Guide": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/snapshot.md",
"Coverage Configuration": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/coverage.md",
"Test Projects (workspace/monorepo)": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/projects.md",
"Browser Mode": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/browser/index.md",
"Lifecycle Hooks": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/lifecycle.md",
"Extending Matchers": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/extending-matchers.md",
"In-Source Testing": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/in-source.md",
"Migration Guide (from Jest)": "https://raw.githubusercontent.com/vitest-dev/vitest/main/docs/guide/migration.md"
},
"lastFetched": "2026-02-16T13:19:42.312Z"
}
Vitest Assertions
The expect API, matchers, snapshots, and extending with custom matchers.
Basics
Vitest provides both Jest-compatible and Chai assertion APIs:
expect(value).toBe(2) // Jest-style (preferred in Vitest)
expect(value).to.equal(2) // Chai-style (also works)Optional second argument for custom error messages:
expect(value, 'should be positive').toBeGreaterThan(0)Value Matchers
Equality
| Matcher | Use When |
|---|---|
toBe(val) | Primitives or same reference (Object.is) |
toEqual(val) | Deep structural equality (ignores undefined in expected) |
toStrictEqual(val) | Deep equality + checks undefined keys, sparse arrays, class types |
toMatchObject(subset) | Object contains at least these properties |
Truthiness
| Matcher | Checks |
|---|---|
toBeTruthy() | Truthy (not false, 0, '', null, undefined, NaN) |
toBeFalsy() | Falsy |
toBeNull() | === null |
toBeUndefined() | === undefined |
toBeDefined() | !== undefined |
toBeNaN() | Number.isNaN |
Numbers
| Matcher | Checks |
|---|---|
toBeGreaterThan(n) | > n |
toBeGreaterThanOrEqual(n) | >= n |
toBeLessThan(n) | < n |
toBeCloseTo(n, digits?) | Floating-point comparison (default 2 decimal digits) |
Strings, Arrays, Objects
| Matcher | Checks |
|---|---|
toContain(item) | Array includes item, or string includes substring |
toContainEqual(obj) | Array contains item with matching structure |
toHaveLength(n) | .length === n |
toHaveProperty(key, val?) | Property exists (with optional value check) |
| `toMatch(regex\ | string)` |
Type Checks
expect(value).toBeTypeOf('string') // typeof check
expect(value).toBeInstanceOf(MyClass) // instanceof check
expect(value).toBeOneOf(['a', 'b', 'c']) // value is one of theseErrors
// Sync — must wrap in a function:
expect(() => throwingFn()).toThrow('message')
expect(() => throwingFn()).toThrow(/pattern/)
expect(() => throwingFn()).toThrow(ErrorClass)
// Async — use rejects:
await expect(asyncFn()).rejects.toThrow('message')Spy/Mock Assertions
| Matcher | Checks |
|---|---|
toHaveBeenCalled() | Called at least once |
toHaveBeenCalledTimes(n) | Called exactly n times |
toHaveBeenCalledWith(...args) | Called with these args (at least once) |
toHaveBeenLastCalledWith(...args) | Last call used these args |
toHaveBeenNthCalledWith(n, ...args) | Nth call (1-indexed) used these args |
toHaveReturned() | Returned successfully (no throw) |
toHaveReturnedWith(val) | Returned this value |
Async Assertions
resolves / rejects
Always `await` — un-awaited assertions pass silently:
await expect(fetchData()).resolves.toEqual({ id: 1 })
await expect(failingFn()).rejects.toThrow('error')expect.poll — Retry Until Pass
Retries the assertion callback until it succeeds or times out:
await expect.poll(() => document.querySelector('.el')).toBeTruthy()
await expect.poll(() => getCount(), { timeout: 5000, interval: 100 }).toBe(5)Does not support snapshot matchers or .resolves/.rejects.
Soft Assertions
expect.soft continues the test after failure, reporting all errors at the end:
expect.soft(a).toBe(1) // fail but continue
expect.soft(b).toBe(2) // also checked
// both failures reportedMix with regular expect — a hard expect failure stops the test and reports all soft failures accumulated so far.
Asymmetric Matchers
Use inside toEqual, toHaveBeenCalledWith, etc.:
expect(obj).toEqual({
id: expect.any(Number),
name: expect.any(String),
tags: expect.arrayContaining(['important']),
meta: expect.objectContaining({ version: 1 }),
email: expect.stringContaining('@'),
slug: expect.stringMatching(/^[a-z-]+$/),
})
// Negation:
expect(arr).toEqual(expect.not.arrayContaining(['secret']))expect.closeTo — Floats in Objects
expect({ sum: 0.1 + 0.2 }).toEqual({ sum: expect.closeTo(0.3, 5) })Assertion Count
Guard against missing assertions in async code:
test('callbacks fire', async () => {
expect.assertions(2) // exactly 2 assertions must run
// or:
expect.hasAssertions() // at least 1 assertion must run
})Snapshots
File Snapshots
expect(result).toMatchSnapshot() // writes to .snap file
expect(result).toMatchSnapshot({ id: expect.any(String) }) // shape matchUpdate with vitest -u or press u in watch mode.
Inline Snapshots
expect(result).toMatchInlineSnapshot(`"expected value"`)
// Vitest auto-updates the string argumentFile Snapshots (Custom Path)
await expect(html).toMatchFileSnapshot('./output/basic.html')Async — must await.
Error Snapshots
expect(() => fn()).toThrowErrorMatchingSnapshot()
expect(() => fn()).toThrowErrorMatchingInlineSnapshot(`"error msg"`)Snapshot Best Practices
- Commit snapshot files. Review them in PRs like any other code.
- Use property matchers for volatile data (IDs, timestamps):
toMatchSnapshot({ createdAt: expect.any(Date) }).
- Prefer inline snapshots for small values — easier to review.
- Avoid large snapshots — they become rubber-stamp reviews.
- Vitest sets
printBasicPrototype: falseby default (cleaner output than Jest).
Extending Matchers
Define Custom Matcher
// In setupFiles or test file:
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling
return {
pass,
message: () => `expected ${received} to be within [${floor}, ${ceiling}]`,
}
},
})TypeScript Declaration
// vitest.d.ts
import 'vitest'
declare module 'vitest' {
interface Matchers<T = any> {
toBeWithinRange(floor: number, ceiling: number): T
}
}Add vitest.d.ts to tsconfig.json include. The Matchers interface covers expect().*, expect.* (asymmetric), and expect.extend simultaneously.
Matcher Context
Inside a matcher function, this provides:
this.isNot—trueif.notwas usedthis.equals(a, b)— deep equality with asymmetric matcher supportthis.utils— formatting utilitiesthis.currentTestName— full test name
Return { actual, expected } alongside pass and message to get automatic diff output on failure.
expect.unreachable
Marks a line that should never execute:
try {
await build(dir)
expect.unreachable('should have thrown')
} catch (err) {
expect(err).toBeInstanceOf(Error)
}Vitest Configuration
Config structure, projects, environments, pools, and performance tuning.
Config File
Vitest reads vitest.config.ts (highest priority) or falls back to vite.config.ts. Use defineConfig from 'vitest/config' — it includes Vitest type extensions:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// Vitest options go here
},
})If using an existing vite.config.ts, add the triple-slash directive:
/// <reference types="vitest/config" />
import { defineConfig } from 'vite'
export default defineConfig({
test: {
// ...
},
})Merging Configs
When using separate Vitest and Vite configs, merge explicitly — Vitest config overrides, it does not extend:
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config.mjs'
export default mergeConfig(viteConfig, defineConfig({
test: { /* ... */ },
}))Extending Defaults
import { configDefaults, defineConfig } from 'vitest/config'
export default defineConfig({
test: {
exclude: [...configDefaults.exclude, 'packages/template/*'],
},
})Key Options
Test File Patterns
test: {
include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], // default
exclude: ['**/node_modules/**', '**/.git/**'], // default (v4+)
dir: './src', // limit search directory — faster than exclude
}Globals
Disabled by default. If enabled, update tsconfig.json:
// vitest.config.ts
test: { globals: true }// tsconfig.json
{ "compilerOptions": { "types": ["vitest/globals"] } }Environment
Controls the global environment for test files:
test: {
environment: 'node', // default: 'node' | 'jsdom' | 'happy-dom'
}jsdom and happy-dom require separate installation: npm i -D happy-dom or npm i -D jsdom.
Timeouts
test: {
testTimeout: 5000, // default: 5000ms
hookTimeout: 10000, // default: 10000ms
}Mock Cleanup
test: {
clearMocks: true, // vi.clearAllMocks() before each test
mockReset: true, // vi.resetAllMocks() before each test
restoreMocks: true, // vi.restoreAllMocks() before each test
unstubEnvs: true, // vi.unstubAllEnvs() before each test
unstubGlobals: true, // vi.unstubAllGlobals() before each test
}Recommendation: Enable restoreMocks: true at minimum. It clears history and restores vi.spyOn originals.
Projects (Multi-Config)
Run different configurations within a single Vitest process. Replaces the deprecated workspace option (v3.2+).
export default defineConfig({
test: {
projects: [
'packages/*', // glob: each folder is a project
{
extends: true, // inherit root config
test: {
name: 'unit',
include: ['**/*.unit.test.ts'],
environment: 'node',
},
},
{
test: {
name: 'browser',
include: ['**/*.browser.test.ts'],
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
},
],
},
})Project Constraints
- Every project must have a unique
name. - Use
defineProject(notdefineConfig) in per-project config files for type safety. - Root-only options (coverage, reporters) cannot be set per-project.
Pools and Parallelism
Pool Types
| Pool | Mechanism | When to Use |
|---|---|---|
forks (default) | child_process.fork | Best compatibility, default choice |
threads | worker_threads | Faster for large suites, some packages may break |
vmThreads | VM context in threads | Isolation + performance (cannot disable isolation) |
test: {
pool: 'forks', // 'forks' | 'threads' | 'vmThreads'
}Parallelism Controls
test: {
fileParallelism: true, // run test files in parallel (default)
maxWorkers: 4, // limit worker count
isolate: true, // isolate each file (default)
}Performance tips:
isolate: false— skip per-file isolation for stateless tests (significant speedup).pool: 'threads'— faster thanforksfor large suites.fileParallelism: false— disable when debugging or tests share state.
Concurrent Tests Within a File
describe.concurrent('suite', () => {
it('test 1', async ({ expect }) => { /* ... */ })
it('test 2', async ({ expect }) => { /* ... */ })
})Use expect from the test context (destructured parameter) with concurrent tests to ensure correct snapshot and assertion tracking.
Environment Variables
Vitest autoloads only VITE_-prefixed vars from .env files. To load all:
import { loadEnv } from 'vite'
import { defineConfig } from 'vitest/config'
export default defineConfig(({ mode }) => ({
test: {
env: loadEnv(mode, process.cwd(), ''),
},
}))In-Source Testing
Test private functions alongside implementation:
// src/math.ts
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) })
}Config:
test: { includeSource: ['src/**/*.{js,ts}'] }For production builds, define 'import.meta.vitest': 'undefined' to enable dead code elimination. Add "types": ["vitest/importMeta"] to tsconfig.json.
Use in-source testing for small utilities only — use separate test files for complex tests.
Sharding (CI)
Split test files across machines:
vitest run --reporter=blob --shard=1/3 # machine 1
vitest run --reporter=blob --shard=2/3 # machine 2
vitest run --reporter=blob --shard=3/3 # machine 3
vitest run --merge-reports # merge resultsVitest splits by file, not by test case. Combine with --coverage for merged coverage reports.
Vitest Coverage
Providers, configuration, including/excluding files, and ignoring code.
Providers
| Provider | Package | Mechanism | Recommendation |
|---|---|---|---|
v8 (default) | @vitest/coverage-v8 | V8 engine's native coverage | Recommended — fast, accurate since v3.2 |
istanbul | @vitest/coverage-istanbul | Babel instrumentation | Use when not on V8 (Firefox, Bun) |
Install the provider package:
npm i -D @vitest/coverage-v8
# or
npm i -D @vitest/coverage-istanbulBasic Setup
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8', // default
enabled: true, // or use --coverage CLI flag
},
},
})// package.json
{
"scripts": {
"test": "vitest",
"coverage": "vitest run --coverage"
}
}Including and Excluding Files
Critical: Without coverage.include, only files loaded during tests appear in the report. Set it to catch uncovered files:
coverage: {
include: ['src/**/*.{ts,tsx}'],
exclude: ['**/types/**', '**/*.d.ts'],
}Vitest automatically excludes test files (matching test.include patterns) from coverage.
Reporters
coverage: {
reporter: ['text', 'html', 'lcov'], // multiple reporters
}Common reporters: text (terminal), html (browser), lcov (CI integration), json, clover.
The html reporter integrates with Vitest UI — open the Coverage tab to browse results.
Thresholds
Enforce minimum coverage levels:
coverage: {
thresholds: {
lines: 80,
branches: 80,
functions: 80,
statements: 80,
},
}The test run fails if any threshold is not met.
Ignoring Code
Both providers support ignore comments. In TypeScript, add @preserve to prevent esbuild from stripping the comment:
V8 Ignore Hints
/* v8 ignore next -- @preserve */
function debugOnly() { /* ignored */ }
/* v8 ignore start -- @preserve */
if (process.env.DEBUG) {
console.log('debug info')
}
/* v8 ignore stop -- @preserve */
/* v8 ignore file -- @preserve */
// Entire file excludedIstanbul Ignore Hints
/* istanbul ignore next -- @preserve */
if (process.env.DEBUG) { /* ignored */ }
/* istanbul ignore start -- @preserve */
// ... ignored block ...
/* istanbul ignore stop -- @preserve */V8 vs Istanbul Comparison
| Factor | V8 | Istanbul |
|---|---|---|
| Speed | Faster (no instrumentation) | Slower (Babel transform step) |
| Memory | Lower | Higher |
| Accuracy | Identical to Istanbul since v3.2 | Battle-tested since 2012 |
| Runtime | V8-based only (Node, Chrome) | Any JS runtime |
| File limiting | Cannot limit — instruments all modules | Can limit to specific files |
Default to V8 unless targeting a non-V8 runtime.
Performance Tips
- Set
coverage.includeto limit the scope — avoids processing unrelated files. - Use
v8provider — no pre-instrumentation step. - Run coverage only in CI, not in watch mode:
{ "scripts": { "coverage": "vitest run --coverage" } }- For very large projects, consider sharding with merged coverage:
vitest run --shard=1/3 --coverage
vitest run --shard=2/3 --coverage
vitest run --shard=3/3 --coverage
vitest run --merge-reports --coverageJest to Vitest Migration
Key differences, API mapping, and common gotchas.
Quick API Translation
| Jest | Vitest | Notes |
|---|---|---|
jest.fn() | vi.fn() | Same API surface |
jest.spyOn(obj, 'method') | vi.spyOn(obj, 'method') | Same API |
jest.mock('./mod') | vi.mock('./mod') | Factory return differs (see below) |
jest.requireActual('./mod') | await vi.importActual('./mod') | Always async |
jest.useFakeTimers() | vi.useFakeTimers() | Same @sinonjs/fake-timers internally |
jest.setTimeout(n) | vi.setConfig({ testTimeout: n }) | Different API |
jest.clearAllMocks() | vi.clearAllMocks() | Same behavior |
jest.resetAllMocks() | vi.resetAllMocks() | See mockReset difference below |
jest.restoreAllMocks() | vi.restoreAllMocks() | Same concept |
Type Changes
// Jest
let fn: jest.Mock<(name: string) => number>
// Vitest
import type { Mock } from 'vitest'
let fn: Mock<(name: string) => number>Key Differences
1. Globals Are Not Default
Jest provides describe, it, expect globally. Vitest requires explicit imports:
import { describe, it, expect, vi } from 'vitest'Or enable globals: true in config and add "types": ["vitest/globals"] to tsconfig.json.
2. Module Mock Factory Returns an Object
In Jest, the factory return value IS the default export. In Vitest, you must return an object with explicit exports:
// Jest
jest.mock('./mod', () => 'hello')
// Vitest
vi.mock('./mod', () => ({
default: 'hello',
}))3. mockReset Behavior Differs
- Jest:
mockResetreplaces implementation with empty function returningundefined. - Vitest:
mockResetrestores the original implementation passed tovi.fn(impl).
const fn = vi.fn(() => 42)
fn.mockReset()
fn() // returns 42 in Vitest, undefined in Jest4. mock.mock State Is Persistent
Jest recreates mock state on .mockClear(). Vitest holds a persistent reference:
const mock = vi.fn()
const state = mock.mock
mock.mockClear()
state === mock.mock // true in Vitest, false in Jest5. Auto-Mocking Is Not Automatic
Jest auto-mocks __mocks__ directories. Vitest requires explicit vi.mock() calls. To replicate Jest behavior, call vi.mock in setupFiles:
// test/setup.ts
vi.mock('axios') // uses __mocks__/axios.js if it exists6. Hook Return Values
beforeAll/beforeEach return values are treated as teardown functions in Vitest:
// WRONG — accidentally returns a value that Vitest treats as teardown
beforeEach(() => setActivePinia(createTestingPinia()))
// CORRECT — explicit void
beforeEach(() => { setActivePinia(createTestingPinia()) })7. Hook Execution Order
Jest runs hooks sequentially (list order). Vitest uses stack order by default (reverse for teardown). To match Jest behavior:
test: {
sequence: { hooks: 'list' },
}8. Test Name Separator
Jest: "describe title test title" (space) Vitest: "describe title > test title" (chevron)
9. Snapshot Differences
- Header:
// Vitest Snapshot v1vs// Jest Snapshot v1 printBasicPrototypedefaults tofalse(cleaner output)toThrowErrorMatchingSnapshotprints[Error: msg]not just"msg"
10. Environment
Jest defaults to jsdom. Vitest defaults to node. Set explicitly:
test: { environment: 'jsdom' }Migration Checklist
1. Replace jest.* calls with vi.* equivalents 2. Replace jest.requireActual with await vi.importActual 3. Update mock factories to return objects with explicit exports 4. Add explicit imports or enable globals: true 5. Wrap beforeEach return values in braces if not void 6. Install vitest and remove jest, ts-jest, babel-jest 7. Move jest.config.js options to vitest.config.ts 8. Update tsconfig.json types if using globals 9. Update snapshot files (vitest -u) 10. Set environment: 'jsdom' if tests need DOM APIs
Mocha + Chai + Sinon Migration
Test Structure
Mocha's before/after map to Vitest's beforeAll/afterAll:
// Mocha // Vitest
before(() => {}) beforeAll(() => {})
after(() => {}) afterAll(() => {})
beforeEach(() => {}) beforeEach(() => {}) // same
afterEach(() => {}) afterEach(() => {}) // sameChai Assertions
Work directly — Vitest includes Chai:
import { expect } from 'vitest'
expect(value).to.equal(42) // Chai-style works
expect(value).toBe(42) // Jest-style also worksSinon Replacements
// Sinon // Vitest
sinon.spy() vi.fn()
sinon.spy(obj, 'method') vi.spyOn(obj, 'method')
stub.returns(42) mock.mockReturnValue(42)
stub.callsFake(fn) mock.mockImplementation(fn)
sinon.useFakeTimers() vi.useFakeTimers()
clock.tick(1000) vi.advanceTimersByTime(1000)
sinon.restore() vi.restoreAllMocks()Sinon-Chai Assertions
Vitest (4.1+) supports Chai-style spy assertions natively:
expect(spy).to.have.been.called
expect(spy).to.have.been.calledOnce
expect(spy).to.have.been.calledWith('arg')
expect(spy).to.have.been.calledBefore(otherSpy)No need for sinon-chai plugin.
Vitest Lifecycle
Hooks, setup files, global setup, test context, and execution order.
Hook Execution Order
Within each test file:
1. File-level code — runs immediately during import 2. `describe` callbacks — run immediately (register tests as side effects) 3. `beforeAll` — once before all tests in the suite 4. For each test:
beforeEachhooks (parent-to-child order)- Test function
afterEachhooks (child-to-parent, reverse order by default)onTestFinishedcallbacks (always reverse order)onTestFailedcallbacks (if test failed)
5. `afterAll` — once after all tests in the suite
Nested Suites
Hooks follow hierarchical nesting — outer beforeEach runs before inner beforeEach, and outer afterEach runs after inner afterEach:
describe('outer', () => {
beforeEach(() => console.log('outer beforeEach'))
describe('inner', () => {
beforeEach(() => console.log('inner beforeEach'))
afterEach(() => console.log('inner afterEach'))
it('test', () => console.log('test'))
})
afterEach(() => console.log('outer afterEach'))
})
// Output: outer beforeEach → inner beforeEach → test →
// inner afterEach → outer afterEachHook Order Configuration
test: {
sequence: {
hooks: 'stack', // default: reverse order for teardown (recommended)
// 'list' — Jest-compatible: same order for setup and teardown
// 'parallel' — run hooks in parallel (fastest, use with caution)
},
}Hooks API
beforeAll / afterAll
Run once per describe block (or per file if at top level):
let db: Database
beforeAll(async () => {
db = await createTestDatabase()
return () => db.close() // return value = teardown function
})Teardown return value: If beforeAll or beforeEach returns a function, it runs as teardown. This is a Vitest-specific feature (not in Jest). Be careful not to accidentally return values:
beforeEach(() => setupPinia()) // BAD if setupPinia returns something
beforeEach(() => { setupPinia() }) // GOOD — explicit voidbeforeEach / afterEach
Run before/after every test in the current scope:
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})onTestFinished
Register cleanup inside a test. Always runs, regardless of pass/fail:
it('creates temp file', async () => {
const file = createTempFile()
onTestFinished(() => fs.unlinkSync(file))
// ...
})onTestFailed
Runs only when the test fails — useful for diagnostics:
it('complex integration', ({ onTestFailed }) => {
onTestFailed(() => {
console.log('Current state:', JSON.stringify(state))
})
// ...
})Test Context
Each test receives a context object as its first parameter:
it('uses context', ({ expect, task }) => {
// expect is scoped to this test — required for concurrent tests
expect(1 + 1).toBe(2)
})Available properties:
expect— test-scoped expect (required for concurrent + snapshots)task— test metadata (name, file, etc.)onTestFinished(fn)— register cleanuponTestFailed(fn)— register failure handler
Extending Test Context
import { beforeEach } from 'vitest'
interface TestContext {
db: Database
}
beforeEach<TestContext>(async (context) => {
context.db = await createTestDatabase()
})
it<TestContext>('queries data', ({ db }) => {
expect(db.query('SELECT 1')).toBeTruthy()
})Setup Files
Files that run before each test file. Use for global hooks, custom matchers, or shared setup:
// vitest.config.ts
test: {
setupFiles: ['./test/setup.ts'],
}// test/setup.ts
import { afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
afterEach(() => cleanup())
// Register custom matchers globally:
expect.extend({ /* ... */ })Setup File Behavior
- Runs in the same process as tests (has access to test globals).
- Runs before each test file (not once globally).
- Setup files run in parallel by default. Set
sequence.setupFiles: 'list'
for sequential execution.
- Editing a setup file triggers a full re-run in watch mode.
Global Setup
Runs once before any test workers start. Use for expensive one-time setup (database seeding, server startup):
// vitest.config.ts
test: {
globalSetup: ['./test/global-setup.ts'],
}// test/global-setup.ts
import type { TestProject } from 'vitest/node'
export default function setup(project: TestProject) {
const server = startServer()
project.provide('port', server.port)
return () => server.close() // teardown
}Sharing Data: provide / inject
Global setup runs in a different scope from tests. Use provide/inject to pass serializable data:
// global-setup.ts
project.provide('wsPort', 3000)
// test file
import { inject } from 'vitest'
const port = inject('wsPort') // 3000Declare the type:
declare module 'vitest' {
export interface ProvidedContext {
wsPort: number
}
}Global Setup vs Setup Files
| Global Setup | Setup Files | |
|---|---|---|
| Scope | Main process | Worker (same as tests) |
| Runs | Once before all tests | Before each test file |
| Access test APIs | No | Yes |
| Share data with tests | provide/inject | Direct globals |
| Use for | Server startup, DB seeding | Custom matchers, cleanup hooks |
Concurrent Tests
describe.concurrent('parallel tests', () => {
it('fast test 1', async ({ expect }) => { /* ... */ })
it('fast test 2', async ({ expect }) => { /* ... */ })
})Rules:
- Use
expectfrom test context (not global) for snapshots and assertions. - Each concurrent test gets its own
beforeEach/afterEach. beforeAll/afterAllstill run once, before/after all concurrent tests.
Test Modifiers
it.skip('disabled test', () => {})
it.only('run only this', () => {})
it.todo('not implemented yet')
it.fails('expected to fail', () => { throw new Error() })
// Conditional:
it.skipIf(process.env.CI)('local only', () => {})
it.runIf(process.env.CI)('CI only', () => {})Parametrized Tests
it.each([
{ input: 1, expected: 2 },
{ input: 2, expected: 4 },
])('doubles $input to $expected', ({ input, expected }) => {
expect(double(input)).toBe(expected)
})
// With template literal:
it.each`
input | expected
${1} | ${2}
${2} | ${4}
`('doubles $input to $expected', ({ input, expected }) => {
expect(double(input)).toBe(expected)
})Retry and Repeat
it('flaky test', { retry: 3 }, () => { /* retries up to 3 times on failure */ })
it('stress test', { repeats: 100 }, () => { /* runs 100 times */ })
// Via describe:
describe('flaky suite', { retry: 2 }, () => { /* ... */ })Vitest Mocking
Functions, modules, timers, environment variables, and globals.
Mock Functions
vi.fn() — Standalone Mock
Creates a trackable function. Optionally accepts an implementation:
const fn = vi.fn() // returns undefined
const fn = vi.fn((x: number) => x + 1) // with implementationvi.spyOn() — Spy on Existing Method
Wraps an existing method while preserving the original. Returns a mock:
const spy = vi.spyOn(console, 'log')
// or spy on getter/setter:
const spy = vi.spyOn(obj, 'prop', 'get')Mock Methods (shared by vi.fn and vi.spyOn)
| Method | Effect |
|---|---|
.mockReturnValue(val) | Always return val |
.mockReturnValueOnce(val) | Return val on next call only |
.mockImplementation(fn) | Replace implementation |
.mockImplementationOnce(fn) | Replace for next call only |
.mockResolvedValue(val) | Return Promise.resolve(val) |
.mockRejectedValue(err) | Return Promise.reject(err) |
.mockClear() | Clear call history, keep implementation |
.mockReset() | Clear history + reset to original implementation |
.mockRestore() | Reset + restore original object descriptor (spyOn only) |
Mock State
fn.mock.calls // array of argument arrays
fn.mock.results // array of { type: 'return'|'throw', value }
fn.mock.lastCall // arguments of last call
fn.mock.instances // array of `this` contexts when called with `new`Cleanup Strategy
Set in config (recommended) or call manually:
// Config approach (preferred):
test: { restoreMocks: true }
// Manual approach:
afterEach(() => { vi.restoreAllMocks() })| Config Option | Equivalent | Effect |
|---|---|---|
clearMocks | vi.clearAllMocks() | Clear history only |
mockReset | vi.resetAllMocks() | Clear history + reset impl |
restoreMocks | vi.restoreAllMocks() | Above + restore spied originals |
Module Mocking
vi.mock() — Replace an Entire Module
Hoisted to the top of the file. Always runs before imports, regardless of where you write it:
import { fetchUser } from './api'
// This executes BEFORE the import above
vi.mock('./api', () => ({
fetchUser: vi.fn(),
}))With importOriginal — Partial Mock
vi.mock(import('./api'), async (importOriginal) => {
const mod = await importOriginal()
return {
...mod,
fetchUser: vi.fn(), // override only this export
}
})Type-safe module promise syntax
Use import() instead of a string for better IDE support and type inference:
vi.mock(import('./api'), async (importOriginal) => {
const mod = await importOriginal() // type is inferred
return { ...mod, fetchUser: vi.fn() }
})Auto-mocking
Call vi.mock without a factory to auto-mock all exports:
vi.mock('./api') // all methods return undefined, arrays are emptySpy mode — Track Without Replacing
vi.mock('./api', { spy: true })
// All exports keep original implementations but are wrapped in vi.fn()vi.doMock() — Non-Hoisted Mock
Not hoisted — runs at its position. Only affects subsequent dynamic imports:
vi.doMock('./config', () => ({ env: 'test' }))
const { env } = await import('./config') // mockedvi.hoisted() — Define Variables Before Imports
Moves code to the top of the file, before vi.mock. Use to define mock references:
const mocks = vi.hoisted(() => ({
fetchUser: vi.fn(),
}))
vi.mock('./api', () => ({
fetchUser: mocks.fetchUser,
}))
// Now you can configure the mock before tests:
mocks.fetchUser.mockResolvedValue({ name: 'Alice' })Default Export Caveat
ESM requires explicit default key:
vi.mock('./mod', () => ({
default: { myMethod: vi.fn() }, // required for default export
namedExport: vi.fn(),
}))__mocks__ Directory
If __mocks__/module.js exists alongside the module (or at project root for node_modules), vi.mock('./module') without a factory uses it automatically.
Module Mock Pitfall: Internal Calls
// foobar.ts
export function foo() { return 'foo' }
export function foobar() { return `${foo()}bar` }Mocking foo externally does NOT affect foobar because foobar references foo directly within the same module. This is by design. Solutions:
- Refactor into separate modules
- Use dependency injection
- Accept that internal calls are not mockable
vi.spyOn on Module Exports
Import as namespace and spy on individual exports:
import * as api from './api'
const spy = vi.spyOn(api, 'fetchUser').mockResolvedValue({ name: 'Bob' })This does NOT work in Browser Mode (native ESM is sealed). Use vi.mock('./api', { spy: true }) instead.
Fake Timers
Setup and Teardown
beforeEach(() => { vi.useFakeTimers() })
afterEach(() => { vi.useRealTimers() })Controlling Time
vi.advanceTimersByTime(1000) // advance by 1s
vi.advanceTimersToNextTimer() // run next scheduled timer
vi.runAllTimers() // run all pending timers
vi.runOnlyPendingTimers() // run currently pending, not new ones
// For async timers (setTimeout with promises):
await vi.advanceTimersByTimeAsync(1000)
await vi.runAllTimersAsync()Mock System Time
vi.useFakeTimers()
vi.setSystemTime(new Date(2024, 0, 1))
expect(new Date().getFullYear()).toBe(2024)
vi.useRealTimers()vi.setSystemTime works even without vi.useFakeTimers() — it will only mock Date.* calls in that case.
Config Defaults
test: {
fakeTimers: {
toFake: ['setTimeout', 'setInterval', 'Date', ...], // default: all except nextTick
loopLimit: 10_000, // max timers in runAllTimers
},
}nextTick is not faked by default. Enable explicitly if needed: vi.useFakeTimers({ toFake: ['nextTick'] }).
Environment Variables
vi.stubEnv('NODE_ENV', 'production') // stub process.env + import.meta.env
vi.unstubAllEnvs() // restore all
// Or set unstubEnvs: true in config for automatic cleanupGlobal Variables
vi.stubGlobal('__VERSION__', '1.0.0')
vi.unstubAllGlobals()
// Or set unstubGlobals: true in configvi.mocked() — Type Helper
Narrows TypeScript types to mock types. Does not change runtime behavior:
import { fetchUser } from './api'
vi.mock('./api')
vi.mocked(fetchUser).mockResolvedValue({ name: 'Alice' })
// With deep mocking:
vi.mocked(obj, { deep: true })vi.waitFor() and vi.waitUntil()
Retry a callback until it succeeds or times out:
await vi.waitFor(() => {
if (!server.isReady) throw new Error('not ready')
}, { timeout: 5000, interval: 50 })
// waitUntil — fails immediately on throw, retries on falsy
const el = await vi.waitUntil(
() => document.querySelector('.element'),
{ timeout: 500 }
)