mrkeyoor.com_
Wed 05 Aug 05:04 UTC
npmTestingupdated 05 Aug 2026

vitest

Vitest is a test runner built on top of Vite. It reuses your app's Vite config, plugins, and transform pipeline, so TypeScript, JSX, and ESM work in tests without a separate Babel or ts-jest setup. The API is deliberately Jest-shaped: describe/it/expect, snapshots, mocking and spies via vi, and coverage through v8 or istanbul. On top of that it adds type-level testing with expect-type, benchmarking via Tinybench, a projects config for monorepos, and a browser mode that runs component tests in real browsers. Watch mode re-runs only affected tests, closer to HMR than a full re-run.

Verdict

The default test runner for new TypeScript and Vite projects in 2026; the Jest-compatible API makes most migrations mechanical. Stay on Jest only if old Node versions or deep Jest internals pin you there.

API stability4/5The describe/expect/vi core has been stable across majors, but v3 to v4 landed within about a year and each major moves config options and defaults, so upgrades need the migration guide
Docs5/5vitest.dev covers guide, config reference, and full API with runnable examples, a maintained Jest migration page, and a Chinese translation
Maintenance5/5Backed by VoidZero Inc. per the license, pushed within the last day, regular patch releases (4.1.10 current) and around 410 open issues and PRs on a busy monorepo
Ecosystem4/588M weekly downloads and default-runner status in Vue, Svelte, Astro, and Nuxt templates; the plugin and extension surface is still smaller than Jest's decade of accumulated tooling

Use it if

  • Your project already builds with Vite: tests share the same config, plugins, and aliases with zero duplicate setup
  • You are migrating from Jest and want a compatible expect/mock/snapshot API that is ESM-first instead of CJS-first
  • You need TypeScript and JSX in tests out of the box, without wiring Babel or ts-jest
  • You want type-level tests, benchmarks, or in-browser component tests inside the same runner
Skip it if

Setup reality

npm install -D vitest and npx vitest just works if you already use Vite. The annoying parts: Node 22.12+ and Vite 6.4+ are hard requirements for v4, so older CI images fail immediately; DOM testing needs an explicit environment setting plus a separate jsdom or happy-dom install; globals like describe are off by default until you set globals: true and add vitest/globals to tsconfig types; coverage lives in a separate @vitest/coverage-v8 package. Monorepos need the projects config to run one Vitest across packages.

Patterns

Write a basic test suiteunit-test

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

describe('math', () => {
  it('adds', () => {
    expect(1 + 1).toBe(2)
  })
})

Imports are explicit by default; set globals: true in config if you want Jest-style implicit describe/it/expect.

Test async success and failureasync-test

import { expect, it } from 'vitest'

it('resolves and rejects', async () => {
  await expect(fetchUser(1)).resolves.toMatchObject({ id: 1 })
  await expect(fetchUser(-1)).rejects.toThrow('not found')
})

Always await resolves/rejects assertions; without await a failing promise can pass silently.

Mock a callback with vi.fnmock-function

import { expect, it, vi } from 'vitest'

it('calls the callback once', () => {
  const cb = vi.fn().mockReturnValue(42)
  run(cb)
  expect(cb).toHaveBeenCalledTimes(1)
  expect(cb).toHaveBeenCalledWith('start')
})

Mock state persists between tests unless you enable mockReset in config or call vi.restoreAllMocks in afterEach.

Mock an imported modulemock-module

import { expect, it, vi } from 'vitest'
import { getUser } from './api'

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

it('uses the mocked module', async () => {
  expect(await getUser(1)).toEqual({ id: 1 })
})

vi.mock calls are hoisted to the top of the file; variables defined nearby are not visible inside the factory unless wrapped in vi.hoisted.

Spy on an object method and restore itspy-on-method

import { afterEach, expect, it, vi } from 'vitest'

afterEach(() => {
  vi.restoreAllMocks()
})

it('logs an error on boot failure', () => {
  const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
  boot()
  expect(spy).toHaveBeenCalled()
})

Restore spies after each test or the patched method leaks into every test that runs later in the file.

Control time with fake timersfake-timers

import { afterEach, beforeEach, expect, it, vi } from 'vitest'

beforeEach(() => vi.useFakeTimers())
afterEach(() => vi.useRealTimers())

it('fires after the debounce window', () => {
  const fn = vi.fn()
  const run = debounce(fn, 1000)
  run()
  vi.advanceTimersByTime(1000)
  expect(fn).toHaveBeenCalledOnce()
})

Fake timers do not fake process.nextTick or queueMicrotask by default, and forgetting useRealTimers breaks later async tests.

Snapshot and inline snapshotsnapshot-test

import { expect, it } from 'vitest'

it('matches snapshots', () => {
  expect(buildRoutes(config)).toMatchSnapshot()
  expect(routeCount(config)).toMatchInlineSnapshot('3')
})

Update stale snapshots with vitest -u; inline snapshots are rewritten inside the test file itself.

Configure environment and setup filesconfigure-runner

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./test/setup.ts'],
  },
})

jsdom is not bundled; install jsdom or happy-dom yourself or every DOM test fails at startup.

Collect coverage with thresholdsrun-coverage

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      thresholds: { lines: 80 },
    },
  },
})
// then: npx vitest run --coverage

Coverage requires the separate @vitest/coverage-v8 (or -istanbul) package; Vitest offers to install it on first run.

Assert types with expectTypeOftype-test

import { expectTypeOf, test } from 'vitest'

test('getUser types', () => {
  expectTypeOf(getUser).parameter(0).toBeNumber()
  expectTypeOf(getUser).returns.resolves.toMatchTypeOf<{ id: number }>()
})

Type tests only execute with vitest --typecheck; in a normal run they are skipped without warning.

Run tests in a suite concurrentlyconcurrent-tests

import { describe, it } from 'vitest'

describe.concurrent('independent api calls', () => {
  it('fetches a', async ({ expect }) => {
    expect(await getA()).toBeTruthy()
  })
  it('fetches b', async ({ expect }) => {
    expect(await getB()).toBeTruthy()
  })
})

In concurrent tests use the expect from the test context so snapshots and assertions attach to the right test.

Alternatives

PackageRegistryPick it when
jestnpmLegacy CJS codebases or teams pinned below Node 22 where the older runner still fits
@playwright/testnpmEnd-to-end browser testing rather than unit and component tests
mochanpmA minimal, assertion-agnostic runner for plain Node libraries