Vitest logoVitestv5INTERMEDIATE

Vitest

Vitest 5 cheat sheet: setup, vitest.config.ts, CLI and watch mode, test API, expect matchers, vi mocks, module mocking, fake timers, snapshots, fixtures, coverage, browser mode, projects, type testing, and migrating from Jest.

20 min read
vitesttestingunit-testingmockingvitetypescriptjavascriptjest-alternativebrowser-testingcoverage
Loading your progress

Setup & Configuration

Install Vitest, write a first test, and configure vitest.config.ts

Install Vitest

Add Vitest to a project, check the version, and wire up npm scripts

bash
# Install (needs Node >= 22.12 and Vite >= 6.4)
npm install -D vitest

npx vitest           # watch mode
npx vitest run       # run once
npx vitest --version # vitest/5.1.0
📌 Vitest 5 needs Node 22.12+ and Vite 6.4+; vite is a peer dependency
⚡ vitest watches locally but runs once in CI or non-TTY shells
🟢 Bun users: run 'bun run test', since 'bun test' starts Bun's runner
💡 The official Vitest VS Code extension runs tests from the gutter
installsetupnpm

Your First Test

Write a test file Vitest picks up by default and read the output

typescript
// sum.ts
export const sum = (a: number, b: number) => a + b

// sum.test.ts
import { expect, test } from 'vitest'
import { sum } from './sum'

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3)
})
🟢 Files need .test. or .spec. in the name to be collected by default
📌 Import test, expect and vi from vitest; globals are off by default
⚡ TypeScript and JSX work out of the box through Vite, no ts-jest
💡 Keep tests next to source files or in a tests/ folder; both work
basicsfirst-test

Configure Vitest with defineConfig, reuse your Vite config, and extend the defaults

typescript
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'node',
    include: ['src/**/*.test.ts'],
    setupFiles: ['./vitest.setup.ts'],
    testTimeout: 5_000,
  },
})
📌 A vitest.config.ts overrides vite.config.ts; merge them to keep plugins
⚠️ Setting include or exclude replaces the defaults; spread configDefaults
💡 Vite options like plugins and resolve.alias go at the top level
🔍 process.env.VITEST is set, so vite.config can branch for tests
configdefineConfig

Enable Jest-style globals, pick a DOM environment, and run setup code before each file

typescript
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,                      // describe/test/expect without imports
    environment: 'jsdom',               // npm i -D jsdom
    setupFiles: ['./vitest.setup.ts'],  // runs before every test file
  },
})

// tsconfig.json: "types": ["vitest/globals"]
📌 Globals are off by default; add vitest/globals to tsconfig types
🟢 node is the default environment; install jsdom or happy-dom for DOM
💡 setupFiles run before every file; globalSetup runs once per run
⚠️ Browser Mode is not an environment; use a browser project instead
globalsenvironmentjsdomsetupFiles

CLI & Watch Mode

Run, watch, and report on tests from the command line

The vitest commands and flags you reach for every day

bash
vitest                      # watch mode (single run in CI)
vitest run                  # run once and exit
vitest math                 # files whose path contains "math"
vitest src/math.test.ts:12  # the test at line 12
vitest -t "adds"            # test names matching a regex
vitest -u                   # update snapshots
vitest run --coverage       # with coverage
📌 Path filters are substring matches, not globs or regex
⚡ vitest related --run is ideal for lint-staged pre-commit hooks
💡 vitest doctor reruns your suite with other pools and suggests a win
🔍 Flags accept camelCase or kebab-case: --passWithNoTests works
clirunwatch

Keyboard shortcuts available while vitest is watching

bash
# Press while vitest is watching
a / Enter   rerun all tests
f           rerun only failed tests
u           update failed snapshots
p           filter by filename
t           filter by test name (regex)
q           quit
⚡ Press f after a big refactor to loop on just the broken tests
💡 i (5.1+) lets you accept or reject each snapshot change
🟢 Watch reruns only tests that import the file you changed
📌 CI or a non-TTY shell turns watch off; use vitest run explicitly
watchshortcutskeyboard

Reporters & CI

Choose reporters, find generated reports, and shard runs across CI machines

bash
vitest run --reporter=verbose      # every test name
vitest run --reporter=dot          # compact
vitest run --reporter=junit        # .vitest/junit/output.xml
vitest run --reporter=json --outputFile=results.json
vitest run --reporter=default --reporter=github-actions
📌 v5: json and junit write files under .vitest/ instead of stdout
⚡ github-actions annotations turn on automatically in GitHub Actions
💡 Shard with blob reports, then --merge-reports for one final report
⚠️ test.only fails the run in CI unless allowOnly is set
reporterscijunitsharding

Filtering & Tags

Run only the tests you need by file, name, project, change, or tag

Filtering Tests

Narrow a run by file path, test name, project, or what changed

bash
vitest run user               # files whose path contains "user"
vitest run -t "login"         # test names matching a regex
vitest run -t "auth > login"  # suite > test (v5 separator)
vitest run --project unit     # one project (or -p unit)
vitest run --changed          # tests affected by uncommitted changes
vitest run --last-failed      # only last run's failures (5.1+)
⚠️ v5 -t joins names with ' > '; 'math adds' no longer matches
⚡ --changed needs git; --since-last-run works without a repository
💡 --last-failed reads the results cache, so it works across runs
📌 file:line filters need the full relative path, not just a name
filtertestNamePatternchangedlast-failed

Test Tags

Label tests with shared options and filter runs with tag expressions (4.1+)

typescript
// vitest.config.ts
test: {
  tags: [
    { name: 'db', timeout: 60_000 },
    { name: 'flaky', retry: process.env.CI ? 2 : 0 },
  ],
}

// user.test.ts
test('saves a user', { tags: ['db'] }, async () => { /* ... */ })

// vitest run --tags-filter=db
📌 Tags arrived in 4.1; undeclared tags fail unless strictTags is off
💡 Put timeout and retry on a tag instead of repeating them per test
⚡ --tags-filter accepts !, && (and) and || (or) expressions
🔍 Use tags for categories, projects for different runtime settings
tagsfilter

Test Structure

Organize tests with test, describe, modifiers, parameterized cases, and concurrency

Define tests, group them in suites, and set per-test options

typescript
import { describe, expect, it, test } from 'vitest'

describe('Cart', () => {
  test('starts empty', () => {
    expect(new Cart().items).toEqual([])
  })

  it('adds items', () => { /* it is an alias of test */ })
})
🟢 it and test are identical; describe groups related tests
📌 Default timeout is 5s (testTimeout); hooks get 10s (hookTimeout)
⚠️ Options go 2nd; a trailing timeout can't be combined with options
💡 Reporters show nested names like Cart > add() > adds a new item
testdescribeit

Focus, skip, stub, or conditionally run tests and suites

typescript
test.only('runs alone in this file', () => {})
test.skip('temporarily disabled', () => {})
test.todo('write this later')
test.fails('known bug #123', () => { expect(add(1, 2)).toBe(4) })
test.skipIf(process.platform === 'win32')('posix only', () => {})
test.runIf(process.env.CI)('CI only', () => {})
⚠️ .only throws in CI by default so focused tests can't be merged
💡 skip(condition, note) skips from inside the test at runtime
📌 test.fails flips the result: it passes while the code is broken
🟢 todo tests show up in the summary as a reminder
onlyskiptodoskipIf

Run the same test or suite against a table of inputs

typescript
test.each([
  [1, 1, 2],
  [2, 3, 5],
])('add(%i, %i) -> %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected)
})

test.for([
  { a: 1, b: 1, sum: 2 },
])('add($a, $b) = $sum', ({ a, b, sum }) => {
  expect(add(a, b)).toBe(sum)
})
💡 Prefer test.for: it exposes the test context for concurrent tests
📌 test.each spreads array rows into args; test.for passes the row
🔍 v5 no longer quotes strings in ${D}name titles: case a1, not 'a1'
⚡ describe.each repeats a whole suite for every case
eachforparameterizeddata-driven

Run tests inside a file in parallel and opt individual tests out

typescript
describe.concurrent('api', () => {
  test('users', async ({ expect }) => { /* ... */ })
  test('posts', async ({ expect }) => { /* ... */ })
})

test.concurrent('runs in parallel', async ({ expect }) => {})
test('stays serial', { concurrent: false }, async () => {})
📌 Use the context expect in concurrent tests so snapshots map right
⚠️ v5 removed test.sequential; use { concurrent: false } instead
🔍 Synchronous tests still run one by one even when marked concurrent
💡 maxConcurrency (default 5) caps how many run at once
concurrentparallelshuffle

Hooks & Lifecycle

Run setup and teardown around tests, suites, and files

beforeAll, beforeEach, afterEach, afterAll, and per-test cleanup

typescript
beforeAll(async () => { await db.connect() })
afterAll(async () => { await db.disconnect() })
beforeEach(() => { store.reset() })
afterEach(() => { vi.useRealTimers() })

// Return a cleanup function instead of a matching after hook
beforeEach(() => {
  const server = startServer()
  return () => server.close()
})
💡 Returning a function from beforeEach/beforeAll acts as teardown
⚠️ Arrow shorthand that returns a value can be taken as cleanup
📌 Hooks inside a describe apply only to tests in that block
⚡ onTestFinished keeps cleanup next to the code that needs it
hooksbeforeEachafterEachbeforeAll

Wrap each test or a whole suite in a context such as a transaction or AsyncLocalStorage (4.1+)

typescript
import { aroundEach, test } from 'vitest'

// Every test runs inside the transaction callback
aroundEach(async (runTest) => {
  await db.transaction(runTest)
})

test('insert user', async () => {
  await db.insert({ name: 'Ada' })
})
📌 Added in 4.1; you must call runTest() or runSuite() or it fails
💡 Use for AsyncLocalStorage, tracing spans, or DB transactions
⚡ Plain setup/teardown? beforeEach with a cleanup return is simpler
🔍 The timeout applies separately to the before and after halves
aroundEacharoundAllhooks

Assertions

expect matchers, asymmetric matchers, soft and polling assertions, and custom matchers

Common Matchers

Equality, truthiness, numbers, strings, collections, types, and errors

typescript
expect(2 + 2).toBe(4)                         // Object.is
expect(user).toEqual({ id: 1, name: 'Ada' })  // deep equality
expect(user).toMatchObject({ name: 'Ada' })    // subset
expect(list).toContain('apple')
expect(list).toHaveLength(3)
expect(0.1 + 0.2).toBeCloseTo(0.3)
expect(() => parse('')).toThrow('empty input')
📌 toBe uses Object.is; use toEqual for objects and arrays
🔍 toStrictEqual also checks undefined props, sparse arrays and classes
⚠️ toThrow needs a function wrapper: expect(() => fn()).toThrow()
💡 toThrowError is a deprecated alias in v5; use toThrow
expectmatcherstoEqualtoThrow

Match parts of a value inside toEqual, toHaveBeenCalledWith, and snapshots

typescript
expect(user).toEqual({
  id: expect.any(Number),
  email: expect.stringMatching(/@example\.com$/),
  createdAt: expect.any(Date),
  roles: expect.arrayContaining(['admin']),
})

expect(fn).toHaveBeenCalledWith(expect.objectContaining({ id: 1 }))
💡 Use asymmetric matchers for ids, dates and other changing values
⚡ expect.schemaMatching validates with Zod, Valibot or ArkType
📌 They work inside toEqual, toHaveBeenCalledWith and snapshots
🔍 expect.not.* inverts a matcher, e.g. expect.not.arrayContaining
asymmetricexpect.anyobjectContaining

Keep going after a failure, retry until a value settles, and require assertions to run

typescript
expect.soft(res.status).toBe(200)        // keep going after a failure
expect.soft(res.body).toEqual({ ok: true })

await expect.poll(() => queue.size()).toBe(0)   // retry until it passes

expect.assertions(2)    // exactly 2 assertions must run
expect.hasAssertions()  // at least 1
📌 v5: expect.poll fails on timeout; its callback gets an AbortSignal
⚠️ Always await expect.poll; it returns a promise
💡 expect.unreachable() and expect.fail() flag code that should not run
🎯 expect.requireAssertions catches tests that never assert
softpollassertions

Custom Matchers

Add your own matchers with expect.extend and type them for Vitest 5

typescript
expect.extend({
  toBeWithinRange(received: number, floor: number, ceiling: number) {
    return {
      pass: received >= floor && received <= ceiling,
      message: () => `expected ${received} to be within ${floor}-${ceiling}`,
    }
  },
})

expect(5).toBeWithinRange(1, 10)
📌 v5 changed the type to Matchers<R, T>; return R from sync matchers
💡 One interface types expect().x, expect.x and expect.extend
⚠️ Vitest 5 no longer reads jest.Matchers declarations
🟢 Register matchers in a setup file so every test file gets them
expect.extendcustom-matcherstypescript

Mock Functions

Create, configure, inspect, and reset mock functions and spies with vi

Create mock functions and control what they return, resolve, or throw

typescript
const getUser = vi.fn()
getUser.mockReturnValue({ id: 1 })
getUser.mockReturnValueOnce({ id: 2 })        // next call only
getUser.mockResolvedValue({ id: 1 })          // returns a promise
getUser.mockRejectedValueOnce(new Error('offline'))
getUser.mockImplementation((id: number) => ({ id }))
getUser.mockThrow(new Error('boom'))          // 4.1+
🟢 vi.fn() with no implementation returns undefined
💡 Pass a signature generic: vi.fn<(id: number) => User>()
📌 Once values are used first, then the default return value
⚡ mockResolvedValue is shorthand for an async implementation
vi.fnmockReturnValuemockResolvedValue

Check how a mock was called, what it returned, and in which order

typescript
expect(fn).toHaveBeenCalled()
expect(fn).toHaveBeenCalledTimes(2)
expect(fn).toHaveBeenCalledWith('ada', { admin: true })
expect(fn).toHaveBeenLastCalledWith('grace')
expect(fn).toHaveBeenCalledExactlyOnceWith('ada')
expect(fn).toHaveReturnedWith(42)
📌 Pass the mock itself, not a call: expect(fn), not expect(fn())
💡 toHaveBeenCalledExactlyOnceWith checks count and args together
🔍 mock.calls is an array of argument arrays, one per call
⚡ Mix in matchers: toHaveBeenCalledWith(expect.any(String))
toHaveBeenCalledWithmock.callsspy-assertions

vi.spyOn

Spy on existing methods, getters, and setters, and restore them afterwards

typescript
const spy = vi.spyOn(cart, 'getTotal')
cart.getTotal()
expect(spy).toHaveBeenCalled()

vi.spyOn(cart, 'getTotal').mockReturnValue(0)            // replace behavior
vi.spyOn(console, 'error').mockImplementation(() => {})  // silence logs
spy.mockRestore()                                         // put the original back
💡 spyOn keeps the real implementation until you mock it
📌 vi.restoreAllMocks only restores vi.spyOn spies, not vi.fn mocks
⚡ using spy = vi.spyOn(...) restores automatically at block end
⚠️ ESM namespace exports can't be spied on in Browser Mode
vi.spyOnspyrestore

Reset mock state between tests and understand the Vitest 5 clearMocks default

typescript
fn.mockClear()      // forget calls, keep implementation
fn.mockReset()      // forget calls + reset to the original impl
spy.mockRestore()   // reset + put the real method back (spyOn)

vi.clearAllMocks()
vi.resetAllMocks()
vi.restoreAllMocks()
📌 v5 turns clearMocks on by default; set it false for v4 behavior
⚠️ Calls recorded in beforeAll or at module level are now cleared
🔍 Unlike Jest, mockReset restores vi.fn(impl) to impl, not a no-op
🎯 restoreMocks: true is a safe default if you use vi.spyOn a lot
mockClearmockResetmockRestoreclearMocks

Return different values per argument without hand-written if/else (5.0+)

typescript
const getUser = vi.fn()

vi.when(getUser)
  .calledWith(1).thenReturn({ id: 1, name: 'Ada' })
  .calledWith(2).thenReturn({ id: 2, name: 'Grace' })

getUser(1) // { id: 1, name: 'Ada' }
getUser(3) // undefined (falls through)
📌 New in Vitest 5; replaces if/else inside mockImplementation
⚠️ Overlapping args like 1 and expect.any(Number) share one slot
💡 Use onUnmatched for a catch-all instead of an overlapping matcher
⚡ The newest behavior for the same args is tried first (LIFO)
vi.whenconditional-mockingcalledWith

Module Mocking

Replace imported modules with vi.mock, vi.hoisted, automocks, and __mocks__ folders

Replace a module, or only some of its exports, with a factory

typescript
import { fetchUser } from './api'

vi.mock('./api', () => ({
  fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Ada' }),
}))

test('uses the mock', async () => {
  expect(await fetchUser(1)).toEqual({ id: 1, name: 'Ada' })
})
📌 vi.mock is hoisted above imports and must sit at the top level
⚠️ v5 throws if vi.mock is called inside describe, test or a function
💡 vi.mock(import('./x'), ...) gives you a typed importOriginal
🔍 Return a default key for default exports, unlike Jest factories
vi.mockmodule-mockingimportOriginal

Share mock functions with hoisted factories and get typed access to mocked imports

typescript
const mocks = vi.hoisted(() => ({ fetchUser: vi.fn() }))

vi.mock('./api', () => ({ fetchUser: mocks.fetchUser }))

test('configure per test', async () => {
  mocks.fetchUser.mockResolvedValue({ id: 1 })
})
📌 vi.hoisted runs before imports, so it can't use imported values
💡 vi.mocked() only changes types; it returns the same function
⚠️ v5 also requires vi.hoisted to be at the module top level
🟢 Set return values in beforeEach so each test starts fresh
vi.hoistedvi.mockedtypescript

Automock a module, spy on all its exports, use manual mocks, and mock dynamically

typescript
vi.mock('./math')                  // automock: functions return undefined
vi.mock('./math', { spy: true })   // keep real code, track calls
vi.mock('axios')                   // uses __mocks__/axios.ts if present
📌 __mocks__ files load only when vi.mock() is called, unlike Jest
💡 { spy: true } is the Browser Mode way to spy on module exports
⚡ vi.doMock isn't hoisted, so it can use variables from the test
⚠️ Modules imported in setupFiles are cached before vi.mock runs
automock__mocks__vi.doMockspy

Fake Timers & Dates

Control setTimeout, setInterval, Date, and Temporal in tests

Fake Timers

Fast-forward timers and intervals without waiting in real time

typescript
vi.useFakeTimers()

setTimeout(callback, 1000)
vi.advanceTimersByTime(1000)   // fast-forward
expect(callback).toHaveBeenCalled()

vi.runAllTimers()              // flush every timer
vi.useRealTimers()             // restore
📌 Use the Async variants when timer callbacks await promises
⚠️ nextTick and queueMicrotask aren't faked unless listed in toFake
💡 expect.poll and vi.waitFor auto-advance fake timers for you
⚡ fakeNodeBuiltins (5.1+) also fakes node:timers/promises imports
fake-timersuseFakeTimersadvanceTimersByTime

Freeze or set the current time for date-dependent code

typescript
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-01-01T09:00:00Z'))
expect(new Date().toISOString()).toBe('2026-01-01T09:00:00.000Z')
vi.useRealTimers()
💡 setSystemTime works with or without fake timers
📌 v5 fakes Temporal.Now alongside Date when Temporal is available
⚠️ Local-time dates depend on TZ; set TZ=UTC in CI for stable tests
🟢 Always restore with vi.useRealTimers() in afterEach
datessetSystemTimetemporal

Globals, Env & Network

Stub global variables and environment variables, and mock HTTP requests

Override globals and environment variables and restore them cleanly

typescript
vi.stubGlobal('innerWidth', 375)
vi.stubEnv('NODE_ENV', 'production')
vi.stubEnv('VITE_API_URL', 'https://staging.example.com')

vi.unstubAllGlobals()
vi.unstubAllEnvs()
💡 Stubs restore cleanly; plain assignment can't be undone for you
📌 stubEnv updates both process.env and import.meta.env
🎯 Turn on unstubEnvs and unstubGlobals to reset automatically
🔍 Pass undefined to vi.stubEnv to simulate a missing variable
stubGlobalstubEnvenv

Stub fetch for quick unit tests or intercept real requests with MSW

typescript
const fetchMock = vi.fn().mockResolvedValue(
  new Response(JSON.stringify({ id: 1 }), { status: 200 }),
)
vi.stubGlobal('fetch', fetchMock)

await getUser(1)
expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/users/1')
🎯 MSW intercepts at the network level, so app code stays untouched
📌 MSW 3 renamed onUnhandledRequest to onUnhandledFrame
💡 server.use() overrides a handler for one test; reset in afterEach
⚡ For one-off unit tests, vi.stubGlobal('fetch', vi.fn()) is enough
fetchmswhttpnetwork

Async Testing

Test promises and async code, and wait for conditions without sleeps

Await async code and assert on promises the Vitest 5 way

typescript
test('loads a user', async () => {
  const user = await getUser(1)
  expect(user.name).toBe('Ada')
})

await expect(getUser(1)).resolves.toMatchObject({ name: 'Ada' })
await expect(getUser(-1)).rejects.toThrow('not found')
📌 v5 fails the test if a resolves/rejects assertion isn't awaited
⚠️ Vitest has no done callback; return or await a Promise instead
💡 The context signal aborts on timeout, bail or Ctrl+C
🟢 A rejected promise from the test function fails the test
asyncpromisesresolvesrejects

Retry assertions or callbacks until async state settles

typescript
await expect.poll(() => getStatus()).toBe('ready')

await vi.waitFor(() => {
  expect(server.isReady).toBe(true)
})

const el = await vi.waitUntil(() => document.querySelector('.toast'))
💡 expect.poll for assertions, vi.waitFor for setup code that throws
📌 vi.waitUntil fails right away if the callback throws
⚡ All three auto-advance fake timers between attempts
⚠️ expect.poll does not support snapshots, toThrow or .resolves
waitForwaitUntilpoll

Snapshots

Capture output in snapshot files, inline in tests, or in plain files

toMatchSnapshot, toMatchInlineSnapshot, property matchers, and error snapshots

typescript
expect(renderCard(user)).toMatchSnapshot()

expect(slugify('Hello World')).toMatchInlineSnapshot(`"hello-world"`)

expect(() => validate('')).toThrowErrorMatchingInlineSnapshot(
  `[Error: Email is required]`,
)
📌 Commit .snap files and review snapshot diffs like code
💡 Inline snapshots keep the expected value next to the assertion
🎯 Use property matchers for ids, dates and other random values
⚠️ Keep snapshots small; huge ones get rubber-stamped on update
snapshotinline-snapshottoMatchSnapshot

Update or review snapshots, snapshot to real files, and add serializers

typescript
// vitest -u              update failing snapshots
// watch mode: u = update all, i = review one by one (5.1+)

await expect(html).toMatchFileSnapshot('./__fixtures__/card.html')
⚡ Press i in watch mode (5.1+) to accept or reject each change
📌 v5: toMatchFileSnapshot must be awaited or the test fails
⚠️ CI never writes snapshots; run vitest -u locally and commit
💡 Use a separate folder for file snapshots, not __snapshots__
snapshotupdatetoMatchFileSnapshotserializer

Test Context & Fixtures

Use the built-in test context and build reusable, typed fixtures with test.extend

task, expect, skip, signal, annotate, and per-test hooks from the first test argument

typescript
test('context', ({ task, expect, skip, signal, onTestFinished }) => {
  console.log(task.name)                       // 'context'
  skip(!process.env.API_KEY, 'needs API_KEY')
  onTestFinished(() => cleanup())
})
📌 The first test argument is the context; destructure what you need
💡 Use the context expect in concurrent tests for correct snapshots
⚡ Pass signal to fetch so a timed-out test stops its request
🔍 v5 also puts a bench fixture on the context for benchmarks
test-contexttasksignal

Create reusable setup with automatic cleanup and inferred types

typescript
// my-test.ts
import { test as base } from 'vitest'

export const test = base
  .extend('baseUrl', 'http://localhost:3000')
  .extend('api', ({ baseUrl }) => createClient(baseUrl))

// users.test.ts
test('lists users', async ({ api }) => {
  expect(await api.get('/users')).toHaveLength(2)
})
📌 The builder API (4.1+) infers fixture types from return values
💡 Fixtures are lazy: only the ones a test destructures are created
⚠️ A file or worker fixture can't depend on a test-scoped one
🔍 onCleanup works once per fixture; split fixtures for more cleanup
fixturestest.extendonCleanup

Share fixtures per file or worker, override them per suite, or use the Playwright-style object API

typescript
const test = base
  .extend('port', { scope: 'worker' }, 5432)                        // per worker
  .extend('db', { scope: 'file' }, ({ port }) => connect(port))     // per file
  .extend('role', 'viewer')                                         // per test

describe('admins', () => {
  test.override({ role: 'admin' })   // this suite only
})
🔍 Scopes are test (default), file and worker; wider ones are shared
📌 test.override must be called at the top of a describe block
🎭 The object syntax matches Playwright fixtures; see the Playwright sheet
⚠️ In non-isolated runs a worker fixture override leaks to later files
fixturesscopetest.override

Start shared services once and pass serializable values to every test file

typescript
// global-setup.ts
import type { TestProject } from 'vitest/node'

export default function setup(project: TestProject) {
  project.provide('apiPort', 4000)
}

// any.test.ts
import { inject } from 'vitest'
const port = inject('apiPort')   // 4000
📌 globalSetup runs in a separate scope; share data via provide/inject
💡 Declare ProvidedContext so inject() is fully typed
⚡ Return a function from setup to tear down after the whole run
🔍 setupFiles run per test file; globalSetup runs once per run
globalSetupprovideinject

Coverage

Measure code coverage with v8 or istanbul, enforce thresholds, and ignore code

Enable Coverage

Install a provider, run with --coverage, and choose which files are reported

typescript
// npm i -D @vitest/coverage-v8
// npx vitest run --coverage

// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',                  // default; or 'istanbul'
      include: ['src/**/*.{ts,tsx}'],
    },
  },
})
📌 Without coverage.include only files your tests import are reported
⚡ v8 is faster and matches istanbul accuracy since Vitest 3.2
⚠️ v5 matches include/exclude relative to the root; recheck globs
🟢 Vitest offers to install the provider package on the first run
coveragev8istanbul

Fail the run when coverage drops and exclude code from the report

typescript
coverage: {
  thresholds: {
    lines: 80,
    functions: 80,
    branches: 75,
    statements: 80,
  },
}

/* v8 ignore next -- @preserve */
if (process.env.DEBUG) console.log(state)
📌 The run fails when coverage drops below a threshold
⚠️ Add -- @preserve or the transpiler can strip the ignore comment
💡 autoUpdate rewrites your config with the new, higher thresholds
🔍 v5: glob thresholds no longer inherit the top-level perFile
coveragethresholdsignore

Browser Mode

Run component and DOM tests in a real browser with Playwright

Install a provider and configure browser instances, headless mode, and trace view

typescript
// npm i -D @vitest/browser-playwright   (or: npx vitest init browser)
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: playwright(),
      instances: [{ browser: 'chromium' }],
    },
  },
})
📌 Tests run in a real browser, with the same test, expect and vi APIs
💡 Start with Playwright: it supports parallel browser sessions
⚠️ 5.1 moved the browser port to 23315; set api.port if needed
🔍 5.1 keeps a step-by-step trace for failing tests by default
browser-modeplaywrightsetup

Render components, query with locators, interact, and assert with expect.element

typescript
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
import { render } from 'vitest-browser-react'

test('increments', async () => {
  const screen = await render(<Counter />)
  await screen.getByRole('button', { name: 'Increment' }).click()
  await expect.element(screen.getByText('Count is 1')).toBeVisible()
})
📌 Always await expect.element; it polls until the element matches
⚠️ v5 locators match text exactly; pass { exact: false } for partial
💡 userEvent triggers real browser events, not simulated ones
🎭 Need full E2E across pages and tabs? See the Playwright sheet
browser-modelocatorscomponent-testingreact

Test Projects

Run several configurations or packages in one Vitest process

Split tests into named projects with their own environment, include, and browser settings

typescript
export default defineConfig({
  test: {
    projects: [
      { test: { name: 'unit', include: ['src/**/*.unit.test.ts'], environment: 'node' } },
      { test: { name: 'dom', include: ['src/**/*.dom.test.ts'], environment: 'jsdom' } },
    ],
  },
})

// vitest run -p unit
📌 v5: inline projects inherit the root config by default
⚠️ Arrays merge, so a project's setupFiles add to the root ones
💡 Every project needs a unique name; set one on inline configs
🔍 Root-only options like reporters and coverage can't go in a project
projectsworkspaceconfig

Point Vitest at every package in a monorepo and configure each with defineProject

typescript
// vitest.config.ts at the repo root
export default defineConfig({
  test: {
    projects: ['packages/*'],
  },
})
💡 Use defineProject in package configs for project-safe typing
📌 Referenced config files don't inherit the root config
🔍 process.cwd() stays at the root even inside a project
⚡ test.projects replaced the old vitest.workspace file (3.2+)
projectsmonorepodefineProject

In-Source Testing

Write tests inside source files next to the code they cover

Use import.meta.vitest blocks and strip them from production builds

typescript
// src/math.ts
export const add = (...n: number[]) => n.reduce((a, b) => a + b, 0)

if (import.meta.vitest) {
  const { test, expect } = import.meta.vitest
  test('add', () => {
    expect(add(1, 2, 3)).toBe(6)
  })
}

// vitest.config.ts: test: { includeSource: ['src/**/*.ts'] }
💡 Great for small pure utilities where tests double as docs
📌 Define import.meta.vitest as undefined so builds drop the tests
⚠️ Keep larger suites in separate files so source stays readable
🟢 Add vitest/importMeta to tsconfig types for TypeScript support
in-sourceimport.meta.vitest

Type Testing

Assert on TypeScript types with expectTypeOf and run type checks as tests

Check parameter, return, and object types at compile time

typescript
import { assertType, expectTypeOf, test } from 'vitest'

test('types', () => {
  expectTypeOf(add).parameters.toEqualTypeOf<[number, number]>()
  expectTypeOf(add).returns.toBeNumber()
  expectTypeOf(user).toExtend<{ name: string }>()
  assertType<string>(formatName(user))
})
📌 *.test-d.ts files are type-checked, never executed
⚠️ toMatchTypeOf is deprecated; use toExtend or toMatchObjectType
🔍 toMatchObjectType wants plain object types; interfaces can fail
💡 @ts-expect-error turns 'this should not compile' into a test
expectTypeOfassertTypetypescript

Enable typecheck mode so type tests and type errors show up in the run

typescript
// vitest --typecheck            runs tests and type tests
// vitest run --typecheck.only   only *.test-d.ts files

export default defineConfig({
  test: {
    typecheck: { enabled: true },
  },
})
📌 Vitest runs tsc --noEmit (or vue-tsc) and maps errors to tests
💡 Source type errors are reported too; ignoreSourceErrors hides them
⚡ --typecheck.only can replace a separate tsc step in CI
⚠️ Dynamic names from test.each aren't evaluated in type tests
typechecktsc

Performance & Debugging

Tune pools, isolation, and workers, and debug failing or hanging tests

Pick a worker pool, trade isolation for speed, and let vitest doctor measure options

typescript
export default defineConfig({
  test: {
    pool: 'forks',        // default; 'threads' | 'vmThreads' | 'vmForks'
    isolate: true,        // fresh module graph per file
    maxWorkers: 4,
    fileParallelism: true,
  },
})

// npx vitest doctor   measures faster options on your suite
💡 vitest doctor reruns your suite with candidate configs (5.0+)
⚡ isolate: false is often the biggest speedup for pure unit tests
⚠️ vm pools are fast but can leak memory on long runs
📌 Worker ids are 1-based in v5; check per-worker DB names
performancepoolisolatedoctor

Debugging Tests

Attach a debugger, use Vitest UI, and track down noisy or hanging runs

bash
vitest --inspect-brk --no-file-parallelism   # attach via chrome://inspect
vitest --ui                                   # visual runner (@vitest/ui)
vitest run --reporter=hanging-process         # why won't it exit?
💡 --no-file-parallelism keeps one file at a time so breakpoints hit
📌 Vitest UI needs the token URL that Vitest prints (v5)
⚡ The VS Code extension runs and debugs one test in a click
🔍 detectAsyncLeaks is slow; turn it on only while debugging
debugginginspectui

Migrating from Jest

Map Jest APIs to Vitest and avoid the differences that break migrated tests

Swap jest.* calls for vi.* and fix module factories, types, and callbacks

typescript
// jest.fn()               -> vi.fn()
// jest.spyOn()            -> vi.spyOn()
// jest.mock()             -> vi.mock()
// jest.requireActual()    -> await vi.importActual()
// jest.useFakeTimers()    -> vi.useFakeTimers()
// jest.setTimeout(5000)   -> vi.setConfig({ testTimeout: 5000 })
// jest.Mock<T>            -> import type { Mock } from 'vitest'
import { describe, expect, test, vi } from 'vitest'
🟢 Most tests run unchanged after swapping jest.* for vi.*
📌 Matchers mirror Jest; see the Jest sheet for the shared basics
⚡ Set globals: true to skip adding imports during the migration
💡 ts-jest and babel-jest go away; Vite transforms TS and JSX
jestmigration

Config and behavior differences to check when moving a Jest suite to Vitest

typescript
// vitest.config.ts settings that ease a Jest migration
export default defineConfig({
  test: {
    globals: true,                    // describe/test/expect without imports
    environment: 'jsdom',             // if your Jest config used jsdom
    sequence: { hooks: 'list' },      // run hooks in Jest's order
    setupFiles: ['./jest.setup.ts'],  // reuse your setup file
  },
})
⚠️ Jest runs hooks in order; Vitest stacks them unless hooks: list
📌 vi.mock factories must return named keys, including default
🔍 Expect some snapshot churn; rerun with -u and review the diff
💡 Migrate file by file; Jest and Vitest can coexist during the move
jestmigrationgotchas