
Vitest
- 26 installs
- 951 repo stars
- Updated July 31, 2026
- sanity-io/next-sanity
This is a copy of vitest by antfu - installs and ranking accrue to the original listing.
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
- 26 all-time installs (skills.sh)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/sanity-io/next-sanity --skill vitestAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 26 |
|---|---|
| repo stars | ★ 951 |
| Last updated | July 31, 2026 |
| Repository | sanity-io/next-sanity ↗ |
What it does
Helps with testing & qa tasks during AI-assisted development.
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
-->