
Vitest
- 30.9k installs
- 5.7k repo stars
- Updated June 23, 2026
- antfu/skills
vitest is an agent skill that Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filteri.
About
Vitest fast unit testing framework powered by Vite with Jest-compatible API Use when writing tests mocking configuring coverage or working with test filtering and fixtures name vitest description Vitest fast unit testing framework powered by Vite with Jest-compatible API Use when writing tests mocking configuring coverage or working with test filtering and fixtures metadata author Anthony Fu version 2026 1 28 source Generated from https github com vitest-dev vitest scripts located at https github com antfu skills Vitest is a next-generation testing framework powered by Vite It provides a Jest-compatible API with native ESM TypeScript and JSX support out of the box Vitest shares the same config transformers resolvers and plugins with your Vite app Key Features Vite-native Uses Vite's transformation pipeline for fast HMR-like test updates Jest-compatible Drop-in replacement for most Jest test suites Smart watch mode Only reruns affected tests based on module graph Native ESM TypeScript JSX support without configuration Multi-threaded workers for parallel test execution Built-in coverage via V8 or Istanbul Snapshot testing mocking and spy
- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates
- Jest-compatible: Drop-in replacement for most Jest test suites
- Smart watch mode: Only reruns affected tests based on module graph
- Native ESM, TypeScript, JSX support without configuration
- Multi-threaded workers for parallel test execution
Vitest by the numbers
- 30,864 all-time installs (skills.sh)
- +1,194 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #18 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
vitest capabilities & compatibility
- Capabilities
- vite native: uses vite's transformation pipeline · jest compatible: drop in replacement for most je · smart watch mode: only reruns affected tests bas · native esm, typescript, jsx support without conf · multi threaded workers for parallel test executi
- Use cases
- documentation
What vitest says it does
--- name: vitest description: Vitest fast unit testing framework powered by Vite with Jest-compatible API.
Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures.
It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box.
Vitest shares the same config, transformers, resolvers, and plugins with your Vite app.
npx skills add https://github.com/antfu/skills --skill vitestAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 30.9k |
|---|---|
| repo stars | ★ 5.7k |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 23, 2026 |
| Repository | antfu/skills ↗ |
What problem does vitest solve for developers using this skill?
Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures.
Who is it for?
Developers who need vitest patterns described in the cached skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures.
What you get
Actionable workflows and conventions from SKILL.md for vitest.
- vitest.config.ts environment configuration
By the numbers
- Covers 4 test environments: node, jsdom, happy-dom, edge-runtime
- Generated 2026-01-28 from sources/vitest
Files
Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app.
Key Features:
- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates
- Jest-compatible: Drop-in replacement for most Jest test suites
- Smart watch mode: Only reruns affected tests based on module graph
- Native ESM, TypeScript, JSX support without configuration
- Multi-threaded workers for parallel test execution
- Built-in coverage via V8 or Istanbul
- Snapshot testing, mocking, and spy utilities
The skill is based on Vitest 3.x, generated at 2026-01-28.
Core
| Topic | Description | Reference |
|---|---|---|
| Configuration | Vitest and Vite config integration, defineConfig usage | core-config |
| CLI | Command line interface, commands and options | core-cli |
| Test API | test/it function, modifiers like skip, only, concurrent | core-test-api |
| Describe API | describe/suite for grouping tests and nested suites | core-describe |
| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | core-expect |
| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | core-hooks |
Features
| Topic | Description | Reference |
|---|---|---|
| Mocking | Mock functions, modules, timers, dates with vi utilities | features-mocking |
| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | features-snapshots |
| Coverage | Code coverage with V8 or Istanbul providers | features-coverage |
| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | features-context |
| Concurrency | Concurrent tests, parallel execution, sharding | features-concurrency |
| Filtering | Filter tests by name, file patterns, tags | features-filtering |
Advanced
| Topic | Description | Reference |
|---|---|---|
| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | advanced-vi |
| Environments | Test environments: node, jsdom, happy-dom, custom | advanced-environments |
| Type Testing | Type-level testing with expectTypeOf and assertType | advanced-type-testing |
| Projects | Multi-project workspaces, different configs per project | advanced-projects |
Generation Info
- Source:
sources/vitest - Git SHA:
4a7321e10672f00f0bb698823a381c2cc245b8f7 - Generated: 2026-01-28
Test Environments
Available Environments
node(default) - Node.js environmentjsdom- Browser-like with DOM APIshappy-dom- Faster alternative to jsdomedge-runtime- Vercel Edge Runtime
Configuration
// vitest.config.ts
defineConfig({
test: {
environment: 'jsdom',
// Environment-specific options
environmentOptions: {
jsdom: {
url: 'http://localhost',
},
},
},
})Installing Environment Packages
# jsdom
npm i -D jsdom
# happy-dom (faster, fewer APIs)
npm i -D happy-domPer-File Environment
Use magic comment at top of file:
// @vitest-environment jsdom
import { expect, test } from 'vitest'
test('DOM test', () => {
const div = document.createElement('div')
expect(div).toBeInstanceOf(HTMLDivElement)
})jsdom Environment
Full browser environment simulation:
// @vitest-environment jsdom
test('DOM manipulation', () => {
document.body.innerHTML = '<div id="app"></div>'
const app = document.getElementById('app')
app.textContent = 'Hello'
expect(app.textContent).toBe('Hello')
})
test('window APIs', () => {
expect(window.location.href).toBeDefined()
expect(localStorage).toBeDefined()
})jsdom Options
defineConfig({
test: {
environmentOptions: {
jsdom: {
url: 'http://localhost:3000',
html: '<!DOCTYPE html><html><body></body></html>',
userAgent: 'custom-agent',
resources: 'usable',
},
},
},
})happy-dom Environment
Faster but fewer APIs:
// @vitest-environment happy-dom
test('basic DOM', () => {
const el = document.createElement('div')
el.className = 'test'
expect(el.className).toBe('test')
})Multiple Environments per Project
Use projects for different environments:
defineConfig({
test: {
projects: [
{
test: {
name: 'unit',
include: ['tests/unit/**/*.test.ts'],
environment: 'node',
},
},
{
test: {
name: 'dom',
include: ['tests/dom/**/*.test.ts'],
environment: 'jsdom',
},
},
],
},
})Custom Environment
Create custom environment package:
// vitest-environment-custom/index.ts
import type { Environment } from 'vitest/runtime'
export default <Environment>{
name: 'custom',
viteEnvironment: 'ssr', // or 'client'
setup() {
// Setup global state
globalThis.myGlobal = 'value'
return {
teardown() {
delete globalThis.myGlobal
},
}
},
}Use with:
defineConfig({
test: {
environment: 'custom',
},
})Environment with VM
For full isolation:
export default <Environment>{
name: 'isolated',
viteEnvironment: 'ssr',
async setupVM() {
const vm = await import('node:vm')
const context = vm.createContext()
return {
getVmContext() {
return context
},
teardown() {},
}
},
setup() {
return { teardown() {} }
},
}Browser Mode (Separate from Environments)
For real browser testing, use Vitest Browser Mode:
defineConfig({
test: {
browser: {
enabled: true,
name: 'chromium', // or 'firefox', 'webkit'
provider: 'playwright',
},
},
})CSS and Assets
In jsdom/happy-dom, configure CSS handling:
defineConfig({
test: {
css: true, // Process CSS
// Or with options
css: {
include: /\.module\.css$/,
modules: {
classNameStrategy: 'non-scoped',
},
},
},
})Fixing External Dependencies
If external deps fail with CSS/asset errors:
defineConfig({
test: {
server: {
deps: {
inline: ['problematic-package'],
},
},
},
})Key Points
- Default is
node- no browser APIs - Use
jsdomfor full browser simulation - Use
happy-domfor faster tests with basic DOM - Per-file environment via
// @vitest-environmentcomment - Use projects for multiple environment configurations
- Browser Mode is for real browser testing, not environment
<!-- Source references:
- https://vitest.dev/guide/environment.html
-->
Projects
Run different test configurations in the same Vitest process.
Basic Projects Setup
// vitest.config.ts
defineConfig({
test: {
projects: [
// Glob patterns for config files
'packages/*',
// Inline config
{
test: {
name: 'unit',
include: ['tests/unit/**/*.test.ts'],
environment: 'node',
},
},
{
test: {
name: 'integration',
include: ['tests/integration/**/*.test.ts'],
environment: 'jsdom',
},
},
],
},
})Monorepo Pattern
defineConfig({
test: {
projects: [
// Each package has its own vitest.config.ts
'packages/core',
'packages/cli',
'packages/utils',
],
},
})Package config:
// packages/core/vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
name: 'core',
include: ['src/**/*.test.ts'],
environment: 'node',
},
})Different Environments
Run same tests in different environments:
defineConfig({
test: {
projects: [
{
test: {
name: 'happy-dom',
root: './shared-tests',
environment: 'happy-dom',
setupFiles: ['./setup.happy-dom.ts'],
},
},
{
test: {
name: 'node',
root: './shared-tests',
environment: 'node',
setupFiles: ['./setup.node.ts'],
},
},
],
},
})Browser + Node Projects
defineConfig({
test: {
projects: [
{
test: {
name: 'unit',
include: ['tests/unit/**/*.test.ts'],
environment: 'node',
},
},
{
test: {
name: 'browser',
include: ['tests/browser/**/*.test.ts'],
browser: {
enabled: true,
name: 'chromium',
provider: 'playwright',
},
},
},
],
},
})Shared Configuration
// vitest.shared.ts
export const sharedConfig = {
testTimeout: 10000,
setupFiles: ['./tests/setup.ts'],
}
// vitest.config.ts
import { sharedConfig } from './vitest.shared'
defineConfig({
test: {
projects: [
{
test: {
...sharedConfig,
name: 'unit',
include: ['tests/unit/**/*.test.ts'],
},
},
{
test: {
...sharedConfig,
name: 'e2e',
include: ['tests/e2e/**/*.test.ts'],
},
},
],
},
})Project-Specific Dependencies
Each project can have different dependencies inlined:
defineConfig({
test: {
projects: [
{
test: {
name: 'project-a',
server: {
deps: {
inline: ['package-a'],
},
},
},
},
],
},
})Running Specific Projects
# Run specific project
vitest --project unit
vitest --project integration
# Multiple projects
vitest --project unit --project e2e
# Exclude project
vitest --project.ignore browserProviding Values to Projects
Share values from config to tests:
// vitest.config.ts
defineConfig({
test: {
projects: [
{
test: {
name: 'staging',
provide: {
apiUrl: 'https://staging.api.com',
debug: true,
},
},
},
{
test: {
name: 'production',
provide: {
apiUrl: 'https://api.com',
debug: false,
},
},
},
],
},
})
// In tests, use inject
import { inject } from 'vitest'
test('uses correct api', () => {
const url = inject('apiUrl')
expect(url).toContain('api.com')
})With Fixtures
const test = base.extend({
apiUrl: ['/default', { injected: true }],
})
test('uses injected url', ({ apiUrl }) => {
// apiUrl comes from project's provide config
})Project Isolation
Each project runs in its own thread pool by default:
defineConfig({
test: {
projects: [
{
test: {
name: 'isolated',
isolate: true, // Full isolation
pool: 'forks',
},
},
],
},
})Global Setup per Project
defineConfig({
test: {
projects: [
{
test: {
name: 'with-db',
globalSetup: ['./tests/db-setup.ts'],
},
},
],
},
})Key Points
- Projects run in same Vitest process
- Each project can have different environment, config
- Use glob patterns for monorepo packages
- Run specific projects with
--projectflag - Use
provideto inject config values into tests - Projects inherit from root config unless overridden
<!-- Source references:
- https://vitest.dev/guide/projects.html
-->
Type Testing
Test TypeScript types without runtime execution.
Setup
Type tests use .test-d.ts extension:
// math.test-d.ts
import { expectTypeOf } from 'vitest'
import { add } from './math'
test('add returns number', () => {
expectTypeOf(add).returns.toBeNumber()
})Configuration
defineConfig({
test: {
typecheck: {
enabled: true,
// Only type check
only: false,
// Checker: 'tsc' or 'vue-tsc'
checker: 'tsc',
// Include patterns
include: ['**/*.test-d.ts'],
// tsconfig to use
tsconfig: './tsconfig.json',
},
},
})expectTypeOf API
import { expectTypeOf } from 'vitest'
// Basic type checks
expectTypeOf<string>().toBeString()
expectTypeOf<number>().toBeNumber()
expectTypeOf<boolean>().toBeBoolean()
expectTypeOf<null>().toBeNull()
expectTypeOf<undefined>().toBeUndefined()
expectTypeOf<void>().toBeVoid()
expectTypeOf<never>().toBeNever()
expectTypeOf<any>().toBeAny()
expectTypeOf<unknown>().toBeUnknown()
expectTypeOf<object>().toBeObject()
expectTypeOf<Function>().toBeFunction()
expectTypeOf<[]>().toBeArray()
expectTypeOf<symbol>().toBeSymbol()Value Type Checking
const value = 'hello'
expectTypeOf(value).toBeString()
const obj = { name: 'test', count: 42 }
expectTypeOf(obj).toMatchTypeOf<{ name: string }>()
expectTypeOf(obj).toHaveProperty('name')Function Types
function greet(name: string): string {
return `Hello, ${name}`
}
expectTypeOf(greet).toBeFunction()
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>()
expectTypeOf(greet).returns.toBeString()
// Parameter checking
expectTypeOf(greet).parameter(0).toBeString()Object Types
interface User {
id: number
name: string
email?: string
}
expectTypeOf<User>().toHaveProperty('id')
expectTypeOf<User>().toHaveProperty('name').toBeString()
// Check shape
expectTypeOf({ id: 1, name: 'test' }).toMatchTypeOf<User>()Equality vs Matching
interface A { x: number }
interface B { x: number; y: string }
// toMatchTypeOf - subset matching
expectTypeOf<B>().toMatchTypeOf<A>() // B extends A
// toEqualTypeOf - exact match
expectTypeOf<A>().not.toEqualTypeOf<B>() // Not exact match
expectTypeOf<A>().toEqualTypeOf<{ x: number }>() // Exact matchBranded Types
type UserId = number & { __brand: 'UserId' }
type PostId = number & { __brand: 'PostId' }
expectTypeOf<UserId>().not.toEqualTypeOf<PostId>()
expectTypeOf<UserId>().not.toEqualTypeOf<number>()Generic Types
function identity<T>(value: T): T {
return value
}
expectTypeOf(identity<string>).returns.toBeString()
expectTypeOf(identity<number>).returns.toBeNumber()Nullable Types
type MaybeString = string | null | undefined
expectTypeOf<MaybeString>().toBeNullable()
expectTypeOf<string>().not.toBeNullable()assertType
Assert a value matches a type (no assertion at runtime):
import { assertType } from 'vitest'
function getUser(): User | null {
return { id: 1, name: 'test' }
}
test('returns user', () => {
const result = getUser()
// @ts-expect-error - should fail type check
assertType<string>(result)
// Correct type
assertType<User | null>(result)
})Using @ts-expect-error
Test that code produces type error:
test('rejects wrong types', () => {
function requireString(s: string) {}
// @ts-expect-error - number not assignable to string
requireString(123)
})Running Type Tests
# Run type tests
vitest typecheck
# Run alongside unit tests
vitest --typecheck
# Type tests only
vitest --typecheck.onlyMixed Test Files
Combine runtime and type tests:
// user.test.ts
import { describe, expect, expectTypeOf, test } from 'vitest'
import { createUser } from './user'
describe('createUser', () => {
test('runtime: creates user', () => {
const user = createUser('John')
expect(user.name).toBe('John')
})
test('types: returns User type', () => {
expectTypeOf(createUser).returns.toMatchTypeOf<{ name: string }>()
})
})Key Points
- Use
.test-d.tsfor type-only tests expectTypeOffor type assertionstoMatchTypeOffor subset matchingtoEqualTypeOffor exact type matching- Use
@ts-expect-errorto test type errors - Run with
vitest typecheckor--typecheck
<!-- Source references:
- https://vitest.dev/guide/testing-types.html
- https://vitest.dev/api/expect-typeof.html
-->
Vi Utilities
The vi helper provides mocking and utility functions.
import { vi } from 'vitest'Mock Functions
// Create mock
const fn = vi.fn()
const fnWithImpl = vi.fn((x) => x * 2)
// Check if mock
vi.isMockFunction(fn) // true
// Mock methods
fn.mockReturnValue(42)
fn.mockReturnValueOnce(1)
fn.mockResolvedValue(data)
fn.mockRejectedValue(error)
fn.mockImplementation(() => 'result')
fn.mockImplementationOnce(() => 'once')
// Clear/reset
fn.mockClear() // Clear call history
fn.mockReset() // Clear history + implementation
fn.mockRestore() // Restore original (for spies)Spying
const obj = { method: () => 'original' }
const spy = vi.spyOn(obj, 'method')
obj.method()
expect(spy).toHaveBeenCalled()
// Mock implementation
spy.mockReturnValue('mocked')
// Spy on getter/setter
vi.spyOn(obj, 'prop', 'get').mockReturnValue('value')Module Mocking
// Hoisted to top of file
vi.mock('./module', () => ({
fn: vi.fn(),
}))
// Partial mock
vi.mock('./module', async (importOriginal) => ({
...(await importOriginal()),
specificFn: vi.fn(),
}))
// Spy mode - keep implementation
vi.mock('./module', { spy: true })
// Import actual module inside mock
const actual = await vi.importActual('./module')
// Import as mock
const mocked = await vi.importMock('./module')Dynamic Mocking
// Not hoisted - use with dynamic imports
vi.doMock('./config', () => ({ key: 'value' }))
const config = await import('./config')
// Unmock
vi.doUnmock('./config')
vi.unmock('./module') // HoistedReset Modules
// Clear module cache
vi.resetModules()
// Wait for dynamic imports
await vi.dynamicImportSettled()Fake Timers
vi.useFakeTimers()
setTimeout(() => console.log('done'), 1000)
// Advance time
vi.advanceTimersByTime(1000)
vi.advanceTimersByTimeAsync(1000) // For async callbacks
vi.advanceTimersToNextTimer()
vi.advanceTimersToNextFrame() // requestAnimationFrame
// Run all timers
vi.runAllTimers()
vi.runAllTimersAsync()
vi.runOnlyPendingTimers()
// Clear timers
vi.clearAllTimers()
// Check state
vi.getTimerCount()
vi.isFakeTimers()
// Restore
vi.useRealTimers()Mock Date/Time
vi.setSystemTime(new Date('2024-01-01'))
expect(new Date().getFullYear()).toBe(2024)
vi.getMockedSystemTime() // Get mocked date
vi.getRealSystemTime() // Get real time (ms)Global/Env Mocking
// Stub global
vi.stubGlobal('fetch', vi.fn())
vi.unstubAllGlobals()
// Stub environment
vi.stubEnv('API_KEY', 'test')
vi.stubEnv('NODE_ENV', 'test')
vi.unstubAllEnvs()Hoisted Code
Run code before imports:
const mock = vi.hoisted(() => vi.fn())
vi.mock('./module', () => ({
fn: mock, // Can reference hoisted variable
}))Waiting Utilities
// Wait for callback to succeed
await vi.waitFor(async () => {
const el = document.querySelector('.loaded')
expect(el).toBeTruthy()
}, { timeout: 5000, interval: 100 })
// Wait for truthy value
const element = await vi.waitUntil(
() => document.querySelector('.loaded'),
{ timeout: 5000 }
)Mock Object
Mock all methods of an object:
const original = {
method: () => 'real',
nested: { fn: () => 'nested' },
}
const mocked = vi.mockObject(original)
mocked.method() // undefined (mocked)
mocked.method.mockReturnValue('mocked')
// Spy mode
const spied = vi.mockObject(original, { spy: true })
spied.method() // 'real'
expect(spied.method).toHaveBeenCalled()Test Configuration
vi.setConfig({
testTimeout: 10_000,
hookTimeout: 10_000,
})
vi.resetConfig()Global Mock Management
vi.clearAllMocks() // Clear all mock call history
vi.resetAllMocks() // Reset + clear implementation
vi.restoreAllMocks() // Restore originals (spies)vi.mocked Type Helper
TypeScript helper for mocked values:
import { myFn } from './module'
vi.mock('./module')
// Type as mock
vi.mocked(myFn).mockReturnValue('typed')
// Deep mocking
vi.mocked(myModule, { deep: true })
// Partial mock typing
vi.mocked(fn, { partial: true }).mockResolvedValue({ ok: true })Key Points
vi.mockis hoisted - usevi.doMockfor dynamic mockingvi.hoistedlets you reference variables in mock factories- Use
vi.spyOnto spy on existing methods - Fake timers require explicit setup and teardown
vi.waitForretries until assertion passes
<!-- Source references:
- https://vitest.dev/api/vi.html
-->
Command Line Interface
Commands
vitest
Start Vitest in watch mode (dev) or run mode (CI):
vitest # Watch mode in dev, run mode in CI
vitest foobar # Run tests containing "foobar" in path
vitest basic/foo.test.ts:10 # Run specific test by file and line numbervitest run
Run tests once without watch mode:
vitest run
vitest run --coveragevitest watch
Explicitly start watch mode:
vitest watchvitest related
Run tests that import specific files (useful with lint-staged):
vitest related src/index.ts src/utils.ts --runvitest bench
Run only benchmark tests:
vitest benchvitest list
List all matching tests without running them:
vitest list # List test names
vitest list --json # Output as JSON
vitest list --filesOnly # List only test filesvitest init
Initialize project setup:
vitest init browser # Set up browser testingCommon Options
# Configuration
--config <path> # Path to config file
--project <name> # Run specific project
# Filtering
--testNamePattern, -t # Run tests matching pattern
--changed # Run tests for changed files
--changed HEAD~1 # Tests for last commit changes
# Reporters
--reporter <name> # default, verbose, dot, json, html
--reporter=html --outputFile=report.html
# Coverage
--coverage # Enable coverage
--coverage.provider v8 # Use v8 provider
--coverage.reporter text,html
# Execution
--shard <index>/<count> # Split tests across machines
--bail <n> # Stop after n failures
--retry <n> # Retry failed tests n times
--sequence.shuffle # Randomize test order
# Watch mode
--no-watch # Disable watch mode
--standalone # Start without running tests
# Environment
--environment <env> # jsdom, happy-dom, node
--globals # Enable global APIs
# Debugging
--inspect # Enable Node inspector
--inspect-brk # Break on start
# Output
--silent # Suppress console output
--no-color # Disable colorsPackage.json Scripts
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage"
}
}Sharding for CI
Split tests across multiple machines:
# Machine 1
vitest run --shard=1/3 --reporter=blob
# Machine 2
vitest run --shard=2/3 --reporter=blob
# Machine 3
vitest run --shard=3/3 --reporter=blob
# Merge reports
vitest --merge-reports --reporter=junitWatch Mode Keyboard Shortcuts
In watch mode, press:
a- Run all testsf- Run only failed testsu- Update snapshotsp- Filter by filename patternt- Filter by test name patternq- Quit
Key Points
- Watch mode is default in dev, run mode in CI (when
process.env.CIis set) - Use
--runflag to ensure single run (important for lint-staged) - Both camelCase (
--testTimeout) and kebab-case (--test-timeout) work - Boolean options can be negated with
--no-prefix
<!-- Source references:
- https://vitest.dev/guide/cli.html
-->
Configuration
Vitest reads configuration from vitest.config.ts or vite.config.ts. It shares the same config format as Vite.
Basic Setup
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// test options
},
})Using with Existing Vite Config
Add Vitest types reference and use the test property:
// vite.config.ts
/// <reference types="vitest/config" />
import { defineConfig } from 'vite'
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
},
})Merging Configs
If you have separate config files, use mergeConfig:
// vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(viteConfig, defineConfig({
test: {
environment: 'jsdom',
},
}))Common Options
defineConfig({
test: {
// Enable global APIs (describe, it, expect) without imports
globals: true,
// Test environment: 'node', 'jsdom', 'happy-dom'
environment: 'node',
// Setup files to run before each test file
setupFiles: ['./tests/setup.ts'],
// Include patterns for test files
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
// Exclude patterns
exclude: ['**/node_modules/**', '**/dist/**'],
// Test timeout in ms
testTimeout: 5000,
// Hook timeout in ms
hookTimeout: 10000,
// Enable watch mode by default
watch: true,
// Coverage configuration
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'html'],
include: ['src/**/*.ts'],
},
// Run tests in isolation (each file in separate process)
isolate: true,
// Pool for running tests: 'threads', 'forks', 'vmThreads'
pool: 'threads',
// Number of threads/processes
poolOptions: {
threads: {
maxThreads: 4,
minThreads: 1,
},
},
// Automatically clear mocks between tests
clearMocks: true,
// Restore mocks between tests
restoreMocks: true,
// Retry failed tests
retry: 0,
// Stop after first failure
bail: 0,
},
})Conditional Configuration
Use mode or process.env.VITEST for test-specific config:
export default defineConfig(({ mode }) => ({
plugins: mode === 'test' ? [] : [myPlugin()],
test: {
// test options
},
}))Projects (Monorepos)
Run different configurations in the same Vitest process:
defineConfig({
test: {
projects: [
'packages/*',
{
test: {
name: 'unit',
include: ['tests/unit/**/*.test.ts'],
environment: 'node',
},
},
{
test: {
name: 'integration',
include: ['tests/integration/**/*.test.ts'],
environment: 'jsdom',
},
},
],
},
})Key Points
- Vitest uses Vite's transformation pipeline - same
resolve.alias, plugins work vitest.config.tstakes priority overvite.config.ts- Use
--configflag to specify a custom config path process.env.VITESTis set totruewhen running tests- Test config uses
testproperty, rest is Vite config
<!-- Source references:
- https://vitest.dev/guide/#configuring-vitest
- https://vitest.dev/config/
-->
Describe API
Group related tests into suites for organization and shared setup.
Basic Usage
import { describe, expect, test } from 'vitest'
describe('Math', () => {
test('adds numbers', () => {
expect(1 + 1).toBe(2)
})
test('subtracts numbers', () => {
expect(3 - 1).toBe(2)
})
})
// Alias: suite
import { suite } from 'vitest'
suite('equivalent to describe', () => {})Nested Suites
describe('User', () => {
describe('when logged in', () => {
test('shows dashboard', () => {})
test('can update profile', () => {})
})
describe('when logged out', () => {
test('shows login page', () => {})
})
})Suite Options
// All tests inherit options
describe('slow tests', { timeout: 30_000 }, () => {
test('test 1', () => {}) // 30s timeout
test('test 2', () => {}) // 30s timeout
})Suite Modifiers
Skip Suites
describe.skip('skipped suite', () => {
test('wont run', () => {})
})
// Conditional
describe.skipIf(process.env.CI)('not in CI', () => {})
describe.runIf(!process.env.CI)('only local', () => {})Focus Suites
describe.only('only this suite runs', () => {
test('runs', () => {})
})Todo Suites
describe.todo('implement later')Concurrent Suites
// All tests run in parallel
describe.concurrent('parallel tests', () => {
test('test 1', async ({ expect }) => {})
test('test 2', async ({ expect }) => {})
})Sequential in Concurrent
describe.concurrent('parallel', () => {
test('concurrent 1', async () => {})
describe.sequential('must be sequential', () => {
test('step 1', async () => {})
test('step 2', async () => {})
})
})Shuffle Tests
describe.shuffle('random order', () => {
test('test 1', () => {})
test('test 2', () => {})
test('test 3', () => {})
})
// Or with option
describe('random', { shuffle: true }, () => {})Parameterized Suites
describe.each
describe.each([
{ name: 'Chrome', version: 100 },
{ name: 'Firefox', version: 90 },
])('$name browser', ({ name, version }) => {
test('has version', () => {
expect(version).toBeGreaterThan(0)
})
})describe.for
describe.for([
['Chrome', 100],
['Firefox', 90],
])('%s browser', ([name, version]) => {
test('has version', () => {
expect(version).toBeGreaterThan(0)
})
})Hooks in Suites
describe('Database', () => {
let db
beforeAll(async () => {
db = await createDb()
})
afterAll(async () => {
await db.close()
})
beforeEach(async () => {
await db.clear()
})
test('insert works', async () => {
await db.insert({ name: 'test' })
expect(await db.count()).toBe(1)
})
})Modifier Combinations
All modifiers can be chained:
describe.skip.concurrent('skipped concurrent', () => {})
describe.only.shuffle('only and shuffled', () => {})
describe.concurrent.skip('equivalent', () => {})Key Points
- Top-level tests belong to an implicit file suite
- Nested suites inherit parent's options (timeout, retry, etc.)
- Hooks are scoped to their suite and nested suites
- Use
describe.concurrentwith context'sexpectfor snapshots - Shuffle order depends on
sequence.seedconfig
<!-- Source references:
- https://vitest.dev/api/describe.html
-->
Expect API
Vitest uses Chai assertions with Jest-compatible API.
Basic Assertions
import { expect, test } from 'vitest'
test('assertions', () => {
// Equality
expect(1 + 1).toBe(2) // Strict equality (===)
expect({ a: 1 }).toEqual({ a: 1 }) // Deep equality
// Truthiness
expect(true).toBeTruthy()
expect(false).toBeFalsy()
expect(null).toBeNull()
expect(undefined).toBeUndefined()
expect('value').toBeDefined()
// Numbers
expect(10).toBeGreaterThan(5)
expect(10).toBeGreaterThanOrEqual(10)
expect(5).toBeLessThan(10)
expect(0.1 + 0.2).toBeCloseTo(0.3, 5)
// Strings
expect('hello world').toMatch(/world/)
expect('hello').toContain('ell')
// Arrays
expect([1, 2, 3]).toContain(2)
expect([{ a: 1 }]).toContainEqual({ a: 1 })
expect([1, 2, 3]).toHaveLength(3)
// Objects
expect({ a: 1, b: 2 }).toHaveProperty('a')
expect({ a: 1, b: 2 }).toHaveProperty('a', 1)
expect({ a: { b: 1 } }).toHaveProperty('a.b', 1)
expect({ a: 1 }).toMatchObject({ a: 1 })
// Types
expect('string').toBeTypeOf('string')
expect(new Date()).toBeInstanceOf(Date)
})Negation
expect(1).not.toBe(2)
expect({ a: 1 }).not.toEqual({ a: 2 })Error Assertions
// Sync errors - wrap in function
expect(() => throwError()).toThrow()
expect(() => throwError()).toThrow('message')
expect(() => throwError()).toThrow(/pattern/)
expect(() => throwError()).toThrow(CustomError)
// Async errors - use rejects
await expect(asyncThrow()).rejects.toThrow('error')Promise Assertions
// Resolves
await expect(Promise.resolve(1)).resolves.toBe(1)
await expect(fetchData()).resolves.toEqual({ data: true })
// Rejects
await expect(Promise.reject('error')).rejects.toBe('error')
await expect(failingFetch()).rejects.toThrow()Spy/Mock Assertions
const fn = vi.fn()
fn('arg1', 'arg2')
fn('arg3')
expect(fn).toHaveBeenCalled()
expect(fn).toHaveBeenCalledTimes(2)
expect(fn).toHaveBeenCalledWith('arg1', 'arg2')
expect(fn).toHaveBeenLastCalledWith('arg3')
expect(fn).toHaveBeenNthCalledWith(1, 'arg1', 'arg2')
expect(fn).toHaveReturned()
expect(fn).toHaveReturnedWith(value)Asymmetric Matchers
Use inside toEqual, toHaveBeenCalledWith, etc:
expect({ id: 1, name: 'test' }).toEqual({
id: expect.any(Number),
name: expect.any(String),
})
expect({ a: 1, b: 2, c: 3 }).toEqual(
expect.objectContaining({ a: 1 })
)
expect([1, 2, 3, 4]).toEqual(
expect.arrayContaining([1, 3])
)
expect('hello world').toEqual(
expect.stringContaining('world')
)
expect('hello world').toEqual(
expect.stringMatching(/world$/)
)
expect({ value: null }).toEqual({
value: expect.anything() // Matches anything except null/undefined
})
// Negate with expect.not
expect([1, 2]).toEqual(
expect.not.arrayContaining([3])
)Soft Assertions
Continue test after failure:
expect.soft(1).toBe(2) // Marks test failed but continues
expect.soft(2).toBe(3) // Also runs
// All failures reported at endPoll Assertions
Retry until passes:
await expect.poll(() => fetchStatus()).toBe('ready')
await expect.poll(
() => document.querySelector('.element'),
{ interval: 100, timeout: 5000 }
).toBeTruthy()Assertion Count
test('async assertions', async () => {
expect.assertions(2) // Exactly 2 assertions must run
await doAsync((data) => {
expect(data).toBeDefined()
expect(data.id).toBe(1)
})
})
test('at least one', () => {
expect.hasAssertions() // At least 1 assertion must run
})Extending Matchers
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling
return {
pass,
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
}
},
})
test('custom matcher', () => {
expect(100).toBeWithinRange(90, 110)
})Snapshot Assertions
expect(data).toMatchSnapshot()
expect(data).toMatchInlineSnapshot(`{ "id": 1 }`)
await expect(result).toMatchFileSnapshot('./expected.json')
expect(() => throw new Error('fail')).toThrowErrorMatchingSnapshot()Key Points
- Use
toBefor primitives,toEqualfor objects/arrays toStrictEqualchecks undefined properties and array sparseness- Always
awaitasync assertions (resolves,rejects,poll) - Use context's
expectin concurrent tests for correct tracking toThrowrequires wrapping sync code in a function
<!-- Source references:
- https://vitest.dev/api/expect.html
-->
Lifecycle Hooks
Basic Hooks
import { afterAll, afterEach, beforeAll, beforeEach, test } from 'vitest'
beforeAll(async () => {
// Runs once before all tests in file/suite
await setupDatabase()
})
afterAll(async () => {
// Runs once after all tests in file/suite
await teardownDatabase()
})
beforeEach(async () => {
// Runs before each test
await clearTestData()
})
afterEach(async () => {
// Runs after each test
await cleanupMocks()
})Cleanup Return Pattern
Return cleanup function from before* hooks:
beforeAll(async () => {
const server = await startServer()
// Returned function runs as afterAll
return async () => {
await server.close()
}
})
beforeEach(async () => {
const connection = await connect()
// Runs as afterEach
return () => connection.close()
})Scoped Hooks
Hooks apply to current suite and nested suites:
describe('outer', () => {
beforeEach(() => console.log('outer before'))
test('test 1', () => {}) // outer before → test
describe('inner', () => {
beforeEach(() => console.log('inner before'))
test('test 2', () => {}) // outer before → inner before → test
})
})Hook Timeout
beforeAll(async () => {
await slowSetup()
}, 30_000) // 30 second timeoutAround Hooks
Wrap tests with setup/teardown context:
import { aroundEach, test } from 'vitest'
// Wrap each test in database transaction
aroundEach(async (runTest) => {
await db.beginTransaction()
await runTest() // Must be called!
await db.rollback()
})
test('insert user', async () => {
await db.insert({ name: 'Alice' })
// Automatically rolled back after test
})aroundAll
Wrap entire suite:
import { aroundAll, test } from 'vitest'
aroundAll(async (runSuite) => {
console.log('before all tests')
await runSuite() // Must be called!
console.log('after all tests')
})Multiple Around Hooks
Nested like onion layers:
aroundEach(async (runTest) => {
console.log('outer before')
await runTest()
console.log('outer after')
})
aroundEach(async (runTest) => {
console.log('inner before')
await runTest()
console.log('inner after')
})
// Order: outer before → inner before → test → inner after → outer afterTest Hooks
Inside test body:
import { onTestFailed, onTestFinished, test } from 'vitest'
test('with cleanup', () => {
const db = connect()
// Runs after test finishes (pass or fail)
onTestFinished(() => db.close())
// Only runs if test fails
onTestFailed(({ task }) => {
console.log('Failed:', task.result?.errors)
})
db.query('SELECT * FROM users')
})Reusable Cleanup Pattern
function useTestDb() {
const db = connect()
onTestFinished(() => db.close())
return db
}
test('query users', () => {
const db = useTestDb()
expect(db.query('SELECT * FROM users')).toBeDefined()
})
test('query orders', () => {
const db = useTestDb() // Fresh connection, auto-closed
expect(db.query('SELECT * FROM orders')).toBeDefined()
})Concurrent Test Hooks
For concurrent tests, use context's hooks:
test.concurrent('concurrent', ({ onTestFinished }) => {
const resource = allocate()
onTestFinished(() => resource.release())
})Extended Test Hooks
With test.extend, hooks are type-aware:
const test = base.extend<{ db: Database }>({
db: async ({}, use) => {
const db = await createDb()
await use(db)
await db.close()
},
})
// These hooks know about `db` fixture
test.beforeEach(({ db }) => {
db.seed()
})
test.afterEach(({ db }) => {
db.clear()
})Hook Execution Order
Default order (stack): 1. beforeAll (in order) 2. beforeEach (in order) 3. Test 4. afterEach (reverse order) 5. afterAll (reverse order)
Configure with sequence.hooks:
defineConfig({
test: {
sequence: {
hooks: 'list', // 'stack' (default), 'list', 'parallel'
},
},
})Key Points
- Hooks are not called during type checking
- Return cleanup function from
before*to avoidafter*duplication aroundEach/aroundAllmust callrunTest()/runSuite()onTestFinishedalways runs, even if test fails- Use context hooks for concurrent tests
<!-- Source references:
- https://vitest.dev/api/hooks.html
-->
Test API
Basic Test
import { expect, test } from 'vitest'
test('adds numbers', () => {
expect(1 + 1).toBe(2)
})
// Alias: it
import { it } from 'vitest'
it('works the same', () => {
expect(true).toBe(true)
})Async Tests
test('async test', async () => {
const result = await fetchData()
expect(result).toBeDefined()
})
// Promises are automatically awaited
test('returns promise', () => {
return fetchData().then(result => {
expect(result).toBeDefined()
})
})Test Options
// Timeout (default: 5000ms)
test('slow test', async () => {
// ...
}, 10_000)
// Or with options object
test('with options', { timeout: 10_000, retry: 2 }, async () => {
// ...
})Test Modifiers
Skip Tests
test.skip('skipped test', () => {
// Won't run
})
// Conditional skip
test.skipIf(process.env.CI)('not in CI', () => {})
test.runIf(process.env.CI)('only in CI', () => {})
// Dynamic skip via context
test('dynamic skip', ({ skip }) => {
skip(someCondition, 'reason')
// ...
})Focus Tests
test.only('only this runs', () => {
// Other tests in file are skipped
})Todo Tests
test.todo('implement later')
test.todo('with body', () => {
// Not run, shows in report
})Failing Tests
test.fails('expected to fail', () => {
expect(1).toBe(2) // Test passes because assertion fails
})Concurrent Tests
// Run tests in parallel
test.concurrent('test 1', async ({ expect }) => {
// Use context.expect for concurrent tests
expect(await fetch1()).toBe('result')
})
test.concurrent('test 2', async ({ expect }) => {
expect(await fetch2()).toBe('result')
})Sequential Tests
// Force sequential in concurrent context
test.sequential('must run alone', async () => {})Parameterized Tests
test.each
test.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('add(%i, %i) = %i', (a, b, expected) => {
expect(a + b).toBe(expected)
})
// With objects
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)
})
// Template literal
test.each`
a | b | expected
${1} | ${1} | ${2}
${1} | ${2} | ${3}
`('add($a, $b) = $expected', ({ a, b, expected }) => {
expect(a + b).toBe(expected)
})test.for
Preferred over .each - doesn't spread arrays:
test.for([
[1, 1, 2],
[1, 2, 3],
])('add(%i, %i) = %i', ([a, b, expected], { expect }) => {
// Second arg is TestContext
expect(a + b).toBe(expected)
})Test Context
First argument provides context utilities:
test('with context', ({ expect, skip, task }) => {
console.log(task.name) // Test name
skip(someCondition) // Skip dynamically
expect(1).toBe(1) // Context-bound expect
})Custom Test with Fixtures
import { test as base } from 'vitest'
const test = base.extend({
db: async ({}, use) => {
const db = await createDb()
await use(db)
await db.close()
},
})
test('query', async ({ db }) => {
const users = await db.query('SELECT * FROM users')
expect(users).toBeDefined()
})Retry Configuration
test('flaky test', { retry: 3 }, async () => {
// Retries up to 3 times on failure
})
// Advanced retry options
test('with delay', {
retry: {
count: 3,
delay: 1000,
condition: /timeout/i, // Only retry on timeout errors
},
}, async () => {})Tags
test('database test', { tags: ['db', 'slow'] }, async () => {})
// Run with: vitest --tags dbKey Points
- Tests with no body are marked as
todo test.onlythrows in CI unlessallowOnly: true- Use context's
expectfor concurrent tests and snapshots - Function name is used as test name if passed as first arg
<!-- Source references:
- https://vitest.dev/api/test.html
-->
Concurrency & Parallelism
File Parallelism
By default, Vitest runs test files in parallel across workers:
defineConfig({
test: {
// Run files in parallel (default: true)
fileParallelism: true,
// Number of worker threads
maxWorkers: 4,
minWorkers: 1,
// Pool type: 'threads', 'forks', 'vmThreads'
pool: 'threads',
},
})Concurrent Tests
Run tests within a file in parallel:
// Individual concurrent tests
test.concurrent('test 1', async ({ expect }) => {
expect(await fetch1()).toBe('result')
})
test.concurrent('test 2', async ({ expect }) => {
expect(await fetch2()).toBe('result')
})
// All tests in suite concurrent
describe.concurrent('parallel suite', () => {
test('test 1', async ({ expect }) => {})
test('test 2', async ({ expect }) => {})
})Important: Use { expect } from context for concurrent tests.
Sequential in Concurrent Context
Force sequential execution:
describe.concurrent('mostly parallel', () => {
test('parallel 1', async () => {})
test('parallel 2', async () => {})
test.sequential('must run alone 1', async () => {})
test.sequential('must run alone 2', async () => {})
})
// Or entire suite
describe.sequential('sequential suite', () => {
test('first', () => {})
test('second', () => {})
})Max Concurrency
Limit concurrent tests:
defineConfig({
test: {
maxConcurrency: 5, // Max concurrent tests per file
},
})Isolation
Each file runs in isolated environment by default:
defineConfig({
test: {
// Disable isolation for faster runs (less safe)
isolate: false,
},
})Sharding
Split tests across machines:
# Machine 1
vitest run --shard=1/3
# Machine 2
vitest run --shard=2/3
# Machine 3
vitest run --shard=3/3CI Example (GitHub Actions)
jobs:
test:
strategy:
matrix:
shard: [1, 2, 3]
steps:
- run: vitest run --shard=${{ matrix.shard }}/3 --reporter=blob
merge:
needs: test
steps:
- run: vitest --merge-reports --reporter=junitMerge Reports
# Each shard outputs blob
vitest run --shard=1/3 --reporter=blob --coverage
vitest run --shard=2/3 --reporter=blob --coverage
# Merge all blobs
vitest --merge-reports --reporter=json --coverageTest Sequence
Control test order:
defineConfig({
test: {
sequence: {
// Run tests in random order
shuffle: true,
// Seed for reproducible shuffle
seed: 12345,
// Hook execution order
hooks: 'stack', // 'stack', 'list', 'parallel'
// All tests concurrent by default
concurrent: true,
},
},
})Shuffle Tests
Randomize to catch hidden dependencies:
// Via CLI
vitest --sequence.shuffle
// Per suite
describe.shuffle('random order', () => {
test('test 1', () => {})
test('test 2', () => {})
test('test 3', () => {})
})Pool Options
Threads (Default)
defineConfig({
test: {
pool: 'threads',
poolOptions: {
threads: {
maxThreads: 8,
minThreads: 2,
isolate: true,
},
},
},
})Forks
Better isolation, slower:
defineConfig({
test: {
pool: 'forks',
poolOptions: {
forks: {
maxForks: 4,
isolate: true,
},
},
},
})VM Threads
Full VM isolation per file:
defineConfig({
test: {
pool: 'vmThreads',
},
})Bail on Failure
Stop after first failure:
vitest --bail 1 # Stop after 1 failure
vitest --bail # Stop on first failure (same as --bail 1)Key Points
- Files run in parallel by default
- Use
.concurrentfor parallel tests within file - Always use context's
expectin concurrent tests - Sharding splits tests across CI machines
- Use
--merge-reportsto combine sharded results - Shuffle tests to find hidden dependencies
<!-- Source references:
- https://vitest.dev/guide/features.html#running-tests-concurrently
- https://vitest.dev/guide/improving-performance.html
-->
Test Context & Fixtures
Built-in Context
Every test receives context as first argument:
test('context', ({ task, expect, skip }) => {
console.log(task.name) // Test name
expect(1).toBe(1) // Context-bound expect
skip() // Skip test dynamically
})Context Properties
task- Test metadata (name, file, etc.)expect- Expect bound to this test (important for concurrent tests)skip(condition?, message?)- Skip the testonTestFinished(fn)- Cleanup after testonTestFailed(fn)- Run on failure only
Custom Fixtures with test.extend
Create reusable test utilities:
import { test as base } from 'vitest'
// Define fixture types
interface Fixtures {
db: Database
user: User
}
// Create extended test
export const test = base.extend<Fixtures>({
// Fixture with setup/teardown
db: async ({}, use) => {
const db = await createDatabase()
await use(db) // Provide to test
await db.close() // Cleanup
},
// Fixture depending on another fixture
user: async ({ db }, use) => {
const user = await db.createUser({ name: 'Test' })
await use(user)
await db.deleteUser(user.id)
},
})Using fixtures:
test('query user', async ({ db, user }) => {
const found = await db.findUser(user.id)
expect(found).toEqual(user)
})Fixture Initialization
Fixtures only initialize when accessed:
const test = base.extend({
expensive: async ({}, use) => {
console.log('initializing') // Only runs if test uses it
await use('value')
},
})
test('no fixture', () => {}) // expensive not called
test('uses fixture', ({ expensive }) => {}) // expensive calledAuto Fixtures
Run fixture for every test:
const test = base.extend({
setup: [
async ({}, use) => {
await globalSetup()
await use()
await globalTeardown()
},
{ auto: true } // Always run
],
})Scoped Fixtures
File Scope
Initialize once per file:
const test = base.extend({
connection: [
async ({}, use) => {
const conn = await connect()
await use(conn)
await conn.close()
},
{ scope: 'file' }
],
})Worker Scope
Initialize once per worker:
const test = base.extend({
sharedResource: [
async ({}, use) => {
await use(globalResource)
},
{ scope: 'worker' }
],
})Injected Fixtures (from Config)
Override fixtures per project:
// test file
const test = base.extend({
apiUrl: ['/default', { injected: true }],
})
// vitest.config.ts
defineConfig({
test: {
projects: [
{
test: {
name: 'prod',
provide: { apiUrl: 'https://api.prod.com' },
},
},
],
},
})Scoped Values per Suite
Override fixture for specific suite:
const test = base.extend({
environment: 'development',
})
describe('production tests', () => {
test.scoped({ environment: 'production' })
test('uses production', ({ environment }) => {
expect(environment).toBe('production')
})
})
test('uses default', ({ environment }) => {
expect(environment).toBe('development')
})Extended Test Hooks
Type-aware hooks with fixtures:
const test = base.extend<{ db: Database }>({
db: async ({}, use) => {
const db = await createDb()
await use(db)
await db.close()
},
})
// Hooks know about fixtures
test.beforeEach(({ db }) => {
db.seed()
})
test.afterEach(({ db }) => {
db.clear()
})Composing Fixtures
Extend from another extended test:
// base-test.ts
export const test = base.extend<{ db: Database }>({
db: async ({}, use) => { /* ... */ },
})
// admin-test.ts
import { test as dbTest } from './base-test'
export const test = dbTest.extend<{ admin: User }>({
admin: async ({ db }, use) => {
const admin = await db.createAdmin()
await use(admin)
},
})Key Points
- Use
{ }destructuring to access fixtures - Fixtures are lazy - only initialize when accessed
- Return cleanup function from fixtures
- Use
{ auto: true }for setup fixtures - Use
{ scope: 'file' }for expensive shared resources - Fixtures compose - extend from extended tests
<!-- Source references:
- https://vitest.dev/guide/test-context.html
-->
Code Coverage
Setup
# Run tests with coverage
vitest run --coverageConfiguration
// vitest.config.ts
defineConfig({
test: {
coverage: {
// Provider: 'v8' (default, faster) or 'istanbul' (more compatible)
provider: 'v8',
// Enable coverage
enabled: true,
// Reporters
reporter: ['text', 'json', 'html'],
// Files to include
include: ['src/**/*.{ts,tsx}'],
// Files to exclude
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.test.ts',
],
// Report uncovered files
all: true,
// Thresholds
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
})Providers
V8 (Default)
npm i -D @vitest/coverage-v8- Faster, no pre-instrumentation
- Uses V8's native coverage
- Recommended for most projects
Istanbul
npm i -D @vitest/coverage-istanbul- Pre-instruments code
- Works in any JS runtime
- More overhead but widely compatible
Reporters
coverage: {
reporter: [
'text', // Terminal output
'text-summary', // Summary only
'json', // JSON file
'html', // HTML report
'lcov', // For CI tools
'cobertura', // XML format
],
reportsDirectory: './coverage',
}Thresholds
Fail tests if coverage is below threshold:
coverage: {
thresholds: {
// Global thresholds
lines: 80,
functions: 75,
branches: 70,
statements: 80,
// Per-file thresholds
perFile: true,
// Auto-update thresholds (for gradual improvement)
autoUpdate: true,
},
}Ignoring Code
V8
/* v8 ignore next -- @preserve */
function ignored() {
return 'not covered'
}
/* v8 ignore start -- @preserve */
// All code here ignored
/* v8 ignore stop -- @preserve */Istanbul
/* istanbul ignore next -- @preserve */
function ignored() {}
/* istanbul ignore if -- @preserve */
if (condition) {
// ignored
}Note: @preserve keeps comments through esbuild.
Package.json Scripts
{
"scripts": {
"test": "vitest",
"test:coverage": "vitest run --coverage",
"test:coverage:watch": "vitest --coverage"
}
}Vitest UI Coverage
Enable HTML coverage in Vitest UI:
coverage: {
enabled: true,
reporter: ['text', 'html'],
}Run with vitest --ui to view coverage visually.
CI Integration
# GitHub Actions
- name: Run tests with coverage
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.infoCoverage with Sharding
Merge coverage from sharded runs:
vitest run --shard=1/3 --coverage --reporter=blob
vitest run --shard=2/3 --coverage --reporter=blob
vitest run --shard=3/3 --coverage --reporter=blob
vitest --merge-reports --coverage --reporter=jsonKey Points
- V8 is faster, Istanbul is more compatible
- Use
--coverageflag orcoverage.enabled: true - Include
all: trueto see uncovered files - Set thresholds to enforce minimum coverage
- Use
@preservecomment to keep ignore hints
<!-- Source references:
- https://vitest.dev/guide/coverage.html
-->
Test Filtering
CLI Filtering
By File Path
# Run files containing "user"
vitest user
# Multiple patterns
vitest user auth
# Specific file
vitest src/user.test.ts
# By line number
vitest src/user.test.ts:25By Test Name
# Tests matching pattern
vitest -t "login"
vitest --testNamePattern "should.*work"
# Regex patterns
vitest -t "/user|auth/"Changed Files
# Uncommitted changes
vitest --changed
# Since specific commit
vitest --changed HEAD~1
vitest --changed abc123
# Since branch
vitest --changed origin/mainRelated Files
Run tests that import specific files:
vitest related src/utils.ts src/api.ts --runUseful with lint-staged:
// .lintstagedrc.js
export default {
'*.{ts,tsx}': 'vitest related --run',
}Focus Tests (.only)
test.only('only this runs', () => {})
describe.only('only this suite', () => {
test('runs', () => {})
})In CI, .only throws error unless configured:
defineConfig({
test: {
allowOnly: true, // Allow .only in CI
},
})Skip Tests
test.skip('skipped', () => {})
// Conditional
test.skipIf(process.env.CI)('not in CI', () => {})
test.runIf(!process.env.CI)('local only', () => {})
// Dynamic skip
test('dynamic', ({ skip }) => {
skip(someCondition, 'reason')
})Tags
Filter by custom tags:
test('database test', { tags: ['db'] }, () => {})
test('slow test', { tags: ['slow', 'integration'] }, () => {})Run tagged tests:
vitest --tags db
vitest --tags "db,slow" # OR
vitest --tags db --tags slow # ORConfigure allowed tags:
defineConfig({
test: {
tags: ['db', 'slow', 'integration'],
strictTags: true, // Fail on unknown tags
},
})Include/Exclude Patterns
defineConfig({
test: {
// Test file patterns
include: ['**/*.{test,spec}.{ts,tsx}'],
// Exclude patterns
exclude: [
'**/node_modules/**',
'**/e2e/**',
'**/*.skip.test.ts',
],
// Include source for in-source testing
includeSource: ['src/**/*.ts'],
},
})Watch Mode Filtering
In watch mode, press:
p- Filter by filename patternt- Filter by test name patterna- Run all testsf- Run only failed tests
Projects Filtering
Run specific project:
vitest --project unit
vitest --project integration --project e2eEnvironment-based Filtering
const isDev = process.env.NODE_ENV === 'development'
const isCI = process.env.CI
describe.skipIf(isCI)('local only tests', () => {})
describe.runIf(isDev)('dev tests', () => {})Combining Filters
# File pattern + test name + changed
vitest user -t "login" --changed
# Related files + run mode
vitest related src/auth.ts --runList Tests Without Running
vitest list # Show all test names
vitest list -t "user" # Filter by name
vitest list --filesOnly # Show only file paths
vitest list --json # JSON outputKey Points
- Use
-tfor test name pattern filtering --changedruns only tests affected by changes--relatedruns tests importing specific files- Tags provide semantic test grouping
- Use
.onlyfor debugging, but configure CI to reject it - Watch mode has interactive filtering
<!-- Source references:
- https://vitest.dev/guide/filtering.html
- https://vitest.dev/guide/cli.html
-->
Mocking
Mock Functions
import { expect, vi } from 'vitest'
// Create mock function
const fn = vi.fn()
fn('hello')
expect(fn).toHaveBeenCalled()
expect(fn).toHaveBeenCalledWith('hello')
// With implementation
const add = vi.fn((a, b) => a + b)
expect(add(1, 2)).toBe(3)
// Mock return values
fn.mockReturnValue(42)
fn.mockReturnValueOnce(1).mockReturnValueOnce(2)
fn.mockResolvedValue({ data: true })
fn.mockRejectedValue(new Error('fail'))
// Mock implementation
fn.mockImplementation((x) => x * 2)
fn.mockImplementationOnce(() => 'first call')Spying on Objects
const cart = {
getTotal: () => 100,
}
const spy = vi.spyOn(cart, 'getTotal')
cart.getTotal()
expect(spy).toHaveBeenCalled()
// Mock implementation
spy.mockReturnValue(200)
expect(cart.getTotal()).toBe(200)
// Restore original
spy.mockRestore()Module Mocking
// vi.mock is hoisted to top of file
vi.mock('./api', () => ({
fetchUser: vi.fn(() => ({ id: 1, name: 'Mock' })),
}))
import { fetchUser } from './api'
test('mocked module', () => {
expect(fetchUser()).toEqual({ id: 1, name: 'Mock' })
})Partial Mock
vi.mock('./utils', async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
specificFunction: vi.fn(),
}
})Auto-mock with Spy
// Keep implementation but spy on calls
vi.mock('./calculator', { spy: true })
import { add } from './calculator'
test('spy on module', () => {
const result = add(1, 2) // Real implementation
expect(result).toBe(3)
expect(add).toHaveBeenCalledWith(1, 2)
})Manual Mocks (__mocks__)
src/
__mocks__/
axios.ts # Mocks 'axios'
api/
__mocks__/
client.ts # Mocks './client'
client.ts// Just call vi.mock with no factory
vi.mock('axios')
vi.mock('./api/client')Dynamic Mocking (vi.doMock)
Not hoisted - use for dynamic imports:
test('dynamic mock', async () => {
vi.doMock('./config', () => ({
apiUrl: 'http://test.local',
}))
const { apiUrl } = await import('./config')
expect(apiUrl).toBe('http://test.local')
vi.doUnmock('./config')
})Mock Timers
import { afterEach, beforeEach, vi } from 'vitest'
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
test('timers', () => {
const fn = vi.fn()
setTimeout(fn, 1000)
expect(fn).not.toHaveBeenCalled()
vi.advanceTimersByTime(1000)
expect(fn).toHaveBeenCalled()
})
// Other timer methods
vi.runAllTimers() // Run all pending timers
vi.runOnlyPendingTimers() // Run only currently pending
vi.advanceTimersToNextTimer() // Advance to next timerAsync Timer Methods
test('async timers', async () => {
vi.useFakeTimers()
let resolved = false
setTimeout(() => Promise.resolve().then(() => { resolved = true }), 100)
await vi.advanceTimersByTimeAsync(100)
expect(resolved).toBe(true)
})Mock Dates
vi.setSystemTime(new Date('2024-01-01'))
expect(new Date().getFullYear()).toBe(2024)
vi.useRealTimers() // RestoreMock Globals
vi.stubGlobal('fetch', vi.fn(() =>
Promise.resolve({ json: () => ({ data: 'mock' }) })
))
// Restore
vi.unstubAllGlobals()Mock Environment Variables
vi.stubEnv('API_KEY', 'test-key')
expect(import.meta.env.API_KEY).toBe('test-key')
// Restore
vi.unstubAllEnvs()Clearing Mocks
const fn = vi.fn()
fn()
fn.mockClear() // Clear call history
fn.mockReset() // Clear history + implementation
fn.mockRestore() // Restore original (for spies)
// Global
vi.clearAllMocks()
vi.resetAllMocks()
vi.restoreAllMocks()Config Auto-Reset
// vitest.config.ts
defineConfig({
test: {
clearMocks: true, // Clear before each test
mockReset: true, // Reset before each test
restoreMocks: true, // Restore after each test
unstubEnvs: true, // Restore env vars
unstubGlobals: true, // Restore globals
},
})Hoisted Variables for Mocks
const mockFn = vi.hoisted(() => vi.fn())
vi.mock('./module', () => ({
getData: mockFn,
}))
import { getData } from './module'
test('hoisted mock', () => {
mockFn.mockReturnValue('test')
expect(getData()).toBe('test')
})Key Points
vi.mockis hoisted - called before imports- Use
vi.doMockfor dynamic, non-hoisted mocking - Always restore mocks to avoid test pollution
- Use
{ spy: true }to keep implementation but track calls vi.hoistedlets you reference variables in mock factories
<!-- Source references:
- https://vitest.dev/guide/mocking.html
- https://vitest.dev/api/vi.html
-->
Snapshot Testing
Snapshot tests capture output and compare against stored references.
Basic Snapshot
import { expect, test } from 'vitest'
test('snapshot', () => {
const result = generateOutput()
expect(result).toMatchSnapshot()
})First run creates .snap file:
// __snapshots__/test.spec.ts.snap
exports['snapshot 1'] = `
{
"id": 1,
"name": "test"
}
`Inline Snapshots
Stored directly in test file:
test('inline snapshot', () => {
const data = { foo: 'bar' }
expect(data).toMatchInlineSnapshot()
})Vitest updates the test file:
test('inline snapshot', () => {
const data = { foo: 'bar' }
expect(data).toMatchInlineSnapshot(`
{
"foo": "bar",
}
`)
})File Snapshots
Compare against explicit file:
test('render html', async () => {
const html = renderComponent()
await expect(html).toMatchFileSnapshot('./expected/component.html')
})Snapshot Hints
Add descriptive hints:
test('multiple snapshots', () => {
expect(header).toMatchSnapshot('header')
expect(body).toMatchSnapshot('body content')
expect(footer).toMatchSnapshot('footer')
})Object Shape Matching
Match partial structure:
test('shape snapshot', () => {
const data = {
id: Math.random(),
created: new Date(),
name: 'test'
}
expect(data).toMatchSnapshot({
id: expect.any(Number),
created: expect.any(Date),
})
})Error Snapshots
test('error message', () => {
expect(() => {
throw new Error('Something went wrong')
}).toThrowErrorMatchingSnapshot()
})
test('inline error', () => {
expect(() => {
throw new Error('Bad input')
}).toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`)
})Updating Snapshots
# Update all snapshots
vitest -u
vitest --update
# In watch mode, press 'u' to update failed snapshotsCustom Serializers
Add custom snapshot formatting:
expect.addSnapshotSerializer({
test(val) {
return val && typeof val.toJSON === 'function'
},
serialize(val, config, indentation, depth, refs, printer) {
return printer(val.toJSON(), config, indentation, depth, refs)
},
})Or via config:
// vitest.config.ts
defineConfig({
test: {
snapshotSerializers: ['./my-serializer.ts'],
},
})Snapshot Format Options
defineConfig({
test: {
snapshotFormat: {
printBasicPrototype: false, // Don't print Array/Object prototypes
escapeString: false,
},
},
})Concurrent Test Snapshots
Use context's expect:
test.concurrent('concurrent 1', async ({ expect }) => {
expect(await getData()).toMatchSnapshot()
})
test.concurrent('concurrent 2', async ({ expect }) => {
expect(await getOther()).toMatchSnapshot()
})Snapshot File Location
Default: __snapshots__/<test-file>.snap
Customize:
defineConfig({
test: {
resolveSnapshotPath: (testPath, snapExtension) => {
return testPath.replace('__tests__', '__snapshots__') + snapExtension
},
},
})Key Points
- Commit snapshot files to version control
- Review snapshot changes in code review
- Use hints for multiple snapshots in one test
- Use
toMatchFileSnapshotfor large outputs (HTML, JSON) - Inline snapshots auto-update in test file
- Use context's
expectfor concurrent tests
<!-- Source references:
- https://vitest.dev/guide/snapshot.html
- https://vitest.dev/api/expect.html#tomatchsnapshot
-->
Related skills
Forks & variants (7)
Vitest has 7 known copies in the catalog totaling 3k installs. They canonicalize to this original listing.
- supabase - 2.1k installs
- hairyf - 555 installs
- pedronauck - 265 installs
- sanity-io - 26 installs
- jetbrains - 12 installs
FAQ
What does vitest do?
Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures.
When should I use vitest?
Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures.
Is vitest safe to install?
Review the Security Audits panel on this page before installing in production.