Vitest
Vitest 5 cheat sheet: setup, vitest.config.ts, CLI and watch mode, test API, expect matchers, vi mocks, module mocking, fake timers, snapshots, fixtures, coverage, browser mode, projects, type testing, and migrating from Jest.
Setup & Configuration
Install Vitest, write a first test, and configure vitest.config.ts
Add Vitest to a project, check the version, and wire up npm scripts
# Install (needs Node >= 22.12 and Vite >= 6.4)
npm install -D vitest
npx vitest # watch mode
npx vitest run # run once
npx vitest --version # vitest/5.1.0Write a test file Vitest picks up by default and read the output
// sum.ts
export const sum = (a: number, b: number) => a + b
// sum.test.ts
import { expect, test } from 'vitest'
import { sum } from './sum'
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3)
})Configure Vitest with defineConfig, reuse your Vite config, and extend the defaults
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
setupFiles: ['./vitest.setup.ts'],
testTimeout: 5_000,
},
})Enable Jest-style globals, pick a DOM environment, and run setup code before each file
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true, // describe/test/expect without imports
environment: 'jsdom', // npm i -D jsdom
setupFiles: ['./vitest.setup.ts'], // runs before every test file
},
})
// tsconfig.json: "types": ["vitest/globals"]CLI & Watch Mode
Run, watch, and report on tests from the command line
The vitest commands and flags you reach for every day
vitest # watch mode (single run in CI)
vitest run # run once and exit
vitest math # files whose path contains "math"
vitest src/math.test.ts:12 # the test at line 12
vitest -t "adds" # test names matching a regex
vitest -u # update snapshots
vitest run --coverage # with coverageKeyboard shortcuts available while vitest is watching
# Press while vitest is watching
a / Enter rerun all tests
f rerun only failed tests
u update failed snapshots
p filter by filename
t filter by test name (regex)
q quitChoose reporters, find generated reports, and shard runs across CI machines
vitest run --reporter=verbose # every test name
vitest run --reporter=dot # compact
vitest run --reporter=junit # .vitest/junit/output.xml
vitest run --reporter=json --outputFile=results.json
vitest run --reporter=default --reporter=github-actionsFiltering & Tags
Run only the tests you need by file, name, project, change, or tag
Narrow a run by file path, test name, project, or what changed
vitest run user # files whose path contains "user"
vitest run -t "login" # test names matching a regex
vitest run -t "auth > login" # suite > test (v5 separator)
vitest run --project unit # one project (or -p unit)
vitest run --changed # tests affected by uncommitted changes
vitest run --last-failed # only last run's failures (5.1+)Label tests with shared options and filter runs with tag expressions (4.1+)
// vitest.config.ts
test: {
tags: [
{ name: 'db', timeout: 60_000 },
{ name: 'flaky', retry: process.env.CI ? 2 : 0 },
],
}
// user.test.ts
test('saves a user', { tags: ['db'] }, async () => { /* ... */ })
// vitest run --tags-filter=dbTest Structure
Organize tests with test, describe, modifiers, parameterized cases, and concurrency
Define tests, group them in suites, and set per-test options
import { describe, expect, it, test } from 'vitest'
describe('Cart', () => {
test('starts empty', () => {
expect(new Cart().items).toEqual([])
})
it('adds items', () => { /* it is an alias of test */ })
})Focus, skip, stub, or conditionally run tests and suites
test.only('runs alone in this file', () => {})
test.skip('temporarily disabled', () => {})
test.todo('write this later')
test.fails('known bug #123', () => { expect(add(1, 2)).toBe(4) })
test.skipIf(process.platform === 'win32')('posix only', () => {})
test.runIf(process.env.CI)('CI only', () => {})Run the same test or suite against a table of inputs
test.each([
[1, 1, 2],
[2, 3, 5],
])('add(%i, %i) -> %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected)
})
test.for([
{ a: 1, b: 1, sum: 2 },
])('add($a, $b) = $sum', ({ a, b, sum }) => {
expect(add(a, b)).toBe(sum)
})Run tests inside a file in parallel and opt individual tests out
describe.concurrent('api', () => {
test('users', async ({ expect }) => { /* ... */ })
test('posts', async ({ expect }) => { /* ... */ })
})
test.concurrent('runs in parallel', async ({ expect }) => {})
test('stays serial', { concurrent: false }, async () => {})Hooks & Lifecycle
Run setup and teardown around tests, suites, and files
beforeAll, beforeEach, afterEach, afterAll, and per-test cleanup
beforeAll(async () => { await db.connect() })
afterAll(async () => { await db.disconnect() })
beforeEach(() => { store.reset() })
afterEach(() => { vi.useRealTimers() })
// Return a cleanup function instead of a matching after hook
beforeEach(() => {
const server = startServer()
return () => server.close()
})Wrap each test or a whole suite in a context such as a transaction or AsyncLocalStorage (4.1+)
import { aroundEach, test } from 'vitest'
// Every test runs inside the transaction callback
aroundEach(async (runTest) => {
await db.transaction(runTest)
})
test('insert user', async () => {
await db.insert({ name: 'Ada' })
})Assertions
expect matchers, asymmetric matchers, soft and polling assertions, and custom matchers
Equality, truthiness, numbers, strings, collections, types, and errors
expect(2 + 2).toBe(4) // Object.is
expect(user).toEqual({ id: 1, name: 'Ada' }) // deep equality
expect(user).toMatchObject({ name: 'Ada' }) // subset
expect(list).toContain('apple')
expect(list).toHaveLength(3)
expect(0.1 + 0.2).toBeCloseTo(0.3)
expect(() => parse('')).toThrow('empty input')Match parts of a value inside toEqual, toHaveBeenCalledWith, and snapshots
expect(user).toEqual({
id: expect.any(Number),
email: expect.stringMatching(/@example\.com$/),
createdAt: expect.any(Date),
roles: expect.arrayContaining(['admin']),
})
expect(fn).toHaveBeenCalledWith(expect.objectContaining({ id: 1 }))Keep going after a failure, retry until a value settles, and require assertions to run
expect.soft(res.status).toBe(200) // keep going after a failure
expect.soft(res.body).toEqual({ ok: true })
await expect.poll(() => queue.size()).toBe(0) // retry until it passes
expect.assertions(2) // exactly 2 assertions must run
expect.hasAssertions() // at least 1Add your own matchers with expect.extend and type them for Vitest 5
expect.extend({
toBeWithinRange(received: number, floor: number, ceiling: number) {
return {
pass: received >= floor && received <= ceiling,
message: () => `expected ${received} to be within ${floor}-${ceiling}`,
}
},
})
expect(5).toBeWithinRange(1, 10)Mock Functions
Create, configure, inspect, and reset mock functions and spies with vi
Create mock functions and control what they return, resolve, or throw
const getUser = vi.fn()
getUser.mockReturnValue({ id: 1 })
getUser.mockReturnValueOnce({ id: 2 }) // next call only
getUser.mockResolvedValue({ id: 1 }) // returns a promise
getUser.mockRejectedValueOnce(new Error('offline'))
getUser.mockImplementation((id: number) => ({ id }))
getUser.mockThrow(new Error('boom')) // 4.1+Check how a mock was called, what it returned, and in which order
expect(fn).toHaveBeenCalled()
expect(fn).toHaveBeenCalledTimes(2)
expect(fn).toHaveBeenCalledWith('ada', { admin: true })
expect(fn).toHaveBeenLastCalledWith('grace')
expect(fn).toHaveBeenCalledExactlyOnceWith('ada')
expect(fn).toHaveReturnedWith(42)Spy on existing methods, getters, and setters, and restore them afterwards
const spy = vi.spyOn(cart, 'getTotal')
cart.getTotal()
expect(spy).toHaveBeenCalled()
vi.spyOn(cart, 'getTotal').mockReturnValue(0) // replace behavior
vi.spyOn(console, 'error').mockImplementation(() => {}) // silence logs
spy.mockRestore() // put the original backReset mock state between tests and understand the Vitest 5 clearMocks default
fn.mockClear() // forget calls, keep implementation
fn.mockReset() // forget calls + reset to the original impl
spy.mockRestore() // reset + put the real method back (spyOn)
vi.clearAllMocks()
vi.resetAllMocks()
vi.restoreAllMocks()Return different values per argument without hand-written if/else (5.0+)
const getUser = vi.fn()
vi.when(getUser)
.calledWith(1).thenReturn({ id: 1, name: 'Ada' })
.calledWith(2).thenReturn({ id: 2, name: 'Grace' })
getUser(1) // { id: 1, name: 'Ada' }
getUser(3) // undefined (falls through)Module Mocking
Replace imported modules with vi.mock, vi.hoisted, automocks, and __mocks__ folders
Replace a module, or only some of its exports, with a factory
import { fetchUser } from './api'
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Ada' }),
}))
test('uses the mock', async () => {
expect(await fetchUser(1)).toEqual({ id: 1, name: 'Ada' })
})Share mock functions with hoisted factories and get typed access to mocked imports
const mocks = vi.hoisted(() => ({ fetchUser: vi.fn() }))
vi.mock('./api', () => ({ fetchUser: mocks.fetchUser }))
test('configure per test', async () => {
mocks.fetchUser.mockResolvedValue({ id: 1 })
})Automock a module, spy on all its exports, use manual mocks, and mock dynamically
vi.mock('./math') // automock: functions return undefined
vi.mock('./math', { spy: true }) // keep real code, track calls
vi.mock('axios') // uses __mocks__/axios.ts if presentFake Timers & Dates
Control setTimeout, setInterval, Date, and Temporal in tests
Fast-forward timers and intervals without waiting in real time
vi.useFakeTimers()
setTimeout(callback, 1000)
vi.advanceTimersByTime(1000) // fast-forward
expect(callback).toHaveBeenCalled()
vi.runAllTimers() // flush every timer
vi.useRealTimers() // restoreFreeze or set the current time for date-dependent code
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-01-01T09:00:00Z'))
expect(new Date().toISOString()).toBe('2026-01-01T09:00:00.000Z')
vi.useRealTimers()Globals, Env & Network
Stub global variables and environment variables, and mock HTTP requests
Override globals and environment variables and restore them cleanly
vi.stubGlobal('innerWidth', 375)
vi.stubEnv('NODE_ENV', 'production')
vi.stubEnv('VITE_API_URL', 'https://staging.example.com')
vi.unstubAllGlobals()
vi.unstubAllEnvs()Stub fetch for quick unit tests or intercept real requests with MSW
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ id: 1 }), { status: 200 }),
)
vi.stubGlobal('fetch', fetchMock)
await getUser(1)
expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/users/1')Async Testing
Test promises and async code, and wait for conditions without sleeps
Await async code and assert on promises the Vitest 5 way
test('loads a user', async () => {
const user = await getUser(1)
expect(user.name).toBe('Ada')
})
await expect(getUser(1)).resolves.toMatchObject({ name: 'Ada' })
await expect(getUser(-1)).rejects.toThrow('not found')Retry assertions or callbacks until async state settles
await expect.poll(() => getStatus()).toBe('ready')
await vi.waitFor(() => {
expect(server.isReady).toBe(true)
})
const el = await vi.waitUntil(() => document.querySelector('.toast'))Snapshots
Capture output in snapshot files, inline in tests, or in plain files
toMatchSnapshot, toMatchInlineSnapshot, property matchers, and error snapshots
expect(renderCard(user)).toMatchSnapshot()
expect(slugify('Hello World')).toMatchInlineSnapshot(`"hello-world"`)
expect(() => validate('')).toThrowErrorMatchingInlineSnapshot(
`[Error: Email is required]`,
)Update or review snapshots, snapshot to real files, and add serializers
// vitest -u update failing snapshots
// watch mode: u = update all, i = review one by one (5.1+)
await expect(html).toMatchFileSnapshot('./__fixtures__/card.html')Test Context & Fixtures
Use the built-in test context and build reusable, typed fixtures with test.extend
task, expect, skip, signal, annotate, and per-test hooks from the first test argument
test('context', ({ task, expect, skip, signal, onTestFinished }) => {
console.log(task.name) // 'context'
skip(!process.env.API_KEY, 'needs API_KEY')
onTestFinished(() => cleanup())
})Create reusable setup with automatic cleanup and inferred types
// my-test.ts
import { test as base } from 'vitest'
export const test = base
.extend('baseUrl', 'http://localhost:3000')
.extend('api', ({ baseUrl }) => createClient(baseUrl))
// users.test.ts
test('lists users', async ({ api }) => {
expect(await api.get('/users')).toHaveLength(2)
})Share fixtures per file or worker, override them per suite, or use the Playwright-style object API
const test = base
.extend('port', { scope: 'worker' }, 5432) // per worker
.extend('db', { scope: 'file' }, ({ port }) => connect(port)) // per file
.extend('role', 'viewer') // per test
describe('admins', () => {
test.override({ role: 'admin' }) // this suite only
})Start shared services once and pass serializable values to every test file
// global-setup.ts
import type { TestProject } from 'vitest/node'
export default function setup(project: TestProject) {
project.provide('apiPort', 4000)
}
// any.test.ts
import { inject } from 'vitest'
const port = inject('apiPort') // 4000Coverage
Measure code coverage with v8 or istanbul, enforce thresholds, and ignore code
Install a provider, run with --coverage, and choose which files are reported
// npm i -D @vitest/coverage-v8
// npx vitest run --coverage
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8', // default; or 'istanbul'
include: ['src/**/*.{ts,tsx}'],
},
},
})Fail the run when coverage drops and exclude code from the report
coverage: {
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
}
/* v8 ignore next -- @preserve */
if (process.env.DEBUG) console.log(state)Browser Mode
Run component and DOM tests in a real browser with Playwright
Install a provider and configure browser instances, headless mode, and trace view
// npm i -D @vitest/browser-playwright (or: npx vitest init browser)
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
})Render components, query with locators, interact, and assert with expect.element
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
import { render } from 'vitest-browser-react'
test('increments', async () => {
const screen = await render(<Counter />)
await screen.getByRole('button', { name: 'Increment' }).click()
await expect.element(screen.getByText('Count is 1')).toBeVisible()
})Test Projects
Run several configurations or packages in one Vitest process
Split tests into named projects with their own environment, include, and browser settings
export default defineConfig({
test: {
projects: [
{ test: { name: 'unit', include: ['src/**/*.unit.test.ts'], environment: 'node' } },
{ test: { name: 'dom', include: ['src/**/*.dom.test.ts'], environment: 'jsdom' } },
],
},
})
// vitest run -p unitPoint Vitest at every package in a monorepo and configure each with defineProject
// vitest.config.ts at the repo root
export default defineConfig({
test: {
projects: ['packages/*'],
},
})In-Source Testing
Write tests inside source files next to the code they cover
Use import.meta.vitest blocks and strip them from production builds
// src/math.ts
export const add = (...n: number[]) => n.reduce((a, b) => a + b, 0)
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest
test('add', () => {
expect(add(1, 2, 3)).toBe(6)
})
}
// vitest.config.ts: test: { includeSource: ['src/**/*.ts'] }Type Testing
Assert on TypeScript types with expectTypeOf and run type checks as tests
Check parameter, return, and object types at compile time
import { assertType, expectTypeOf, test } from 'vitest'
test('types', () => {
expectTypeOf(add).parameters.toEqualTypeOf<[number, number]>()
expectTypeOf(add).returns.toBeNumber()
expectTypeOf(user).toExtend<{ name: string }>()
assertType<string>(formatName(user))
})Enable typecheck mode so type tests and type errors show up in the run
// vitest --typecheck runs tests and type tests
// vitest run --typecheck.only only *.test-d.ts files
export default defineConfig({
test: {
typecheck: { enabled: true },
},
})Performance & Debugging
Tune pools, isolation, and workers, and debug failing or hanging tests
Pick a worker pool, trade isolation for speed, and let vitest doctor measure options
export default defineConfig({
test: {
pool: 'forks', // default; 'threads' | 'vmThreads' | 'vmForks'
isolate: true, // fresh module graph per file
maxWorkers: 4,
fileParallelism: true,
},
})
// npx vitest doctor measures faster options on your suiteAttach a debugger, use Vitest UI, and track down noisy or hanging runs
vitest --inspect-brk --no-file-parallelism # attach via chrome://inspect
vitest --ui # visual runner (@vitest/ui)
vitest run --reporter=hanging-process # why won't it exit?Migrating from Jest
Map Jest APIs to Vitest and avoid the differences that break migrated tests
Swap jest.* calls for vi.* and fix module factories, types, and callbacks
// jest.fn() -> vi.fn()
// jest.spyOn() -> vi.spyOn()
// jest.mock() -> vi.mock()
// jest.requireActual() -> await vi.importActual()
// jest.useFakeTimers() -> vi.useFakeTimers()
// jest.setTimeout(5000) -> vi.setConfig({ testTimeout: 5000 })
// jest.Mock<T> -> import type { Mock } from 'vitest'
import { describe, expect, test, vi } from 'vitest'Config and behavior differences to check when moving a Jest suite to Vitest
// vitest.config.ts settings that ease a Jest migration
export default defineConfig({
test: {
globals: true, // describe/test/expect without imports
environment: 'jsdom', // if your Jest config used jsdom
sequence: { hooks: 'list' }, // run hooks in Jest's order
setupFiles: ['./jest.setup.ts'], // reuse your setup file
},
})