mrkeyoor.com_
Sat 19 Sept 08:56 UTC
npmTestingupdated 19 Sept 2026

vitest review

Vitest runs JavaScript and TypeScript tests through Vite's transform and module-resolution pipeline. That makes it a close fit for projects whose source already depends on Vite aliases, plugins, JSX, or ESM. Its test, expect, snapshot, spy, and module-mocking APIs will look familiar to Jest users, while projects, sharding, type tests, and an optional browser mode cover larger suites. Version 4.1.11 fixes lifecycle concurrency limits, browser iframe URL handling, Chromium cleanup on low disk space, and the file-system boundary used for mock redirects. In our Node 22 sandbox, ESM import worked and require() failed.

94.5Mdownloads / wk
Verdict

Vitest 4.1.11 is a strong match for Vite and ESM projects that will use its shared transform pipeline. Keep it out of require-only tools, and budget separate packages for DOM, browser, UI, or coverage work.

We installed it

Lab card: what happened when we installed vitestScreenshot of vitest documentation
Install✓ · 9.1s47 packages on disk · 39 MB
Import½ESM import works · require() fails · ESM package with exports map
Browser94.4 KBgzipped (306 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does vitest install cleanly?

Yes. In a fresh container with an empty cache, npm install vitest finished in 9 seconds, leaving 47 packages and 39 MB on disk. npm audit reported no known vulnerabilities.

How much does vitest add to a browser bundle?

94.4 KB gzipped (306 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does vitest work with both ESM and CommonJS?

ESM only. import 'vitest' worked, require('vitest') failed in our run, so CommonJS projects need a dynamic import or a build step.

Does vitest include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

vitest or jest: which should you use?

jest: Choose it for an established Jest suite that depends on its custom runners, transforms, or CommonJS-era behavior. Vitest 4.1.11 is a strong match for Vite and ESM projects that will use its shared transform pipeline.

When should you not use vitest?

Your runner must load through require(): our CommonJS load test failed on Node.js 22.23.2 even though the package exposes compatibility entries

API stability4/5The everyday describe, test, expect, snapshot, and vi APIs are well established, and the project publishes migration notes for major changes. There are still edges to review on upgrades: v4 changed mock restoration and pool configuration, v4.1 added test tags and an experimental native module runner, and a v5 release candidate is already visible in the repository. Pin the major and read its migration page before changing it.
Docs5/5The official site separates getting-started material, configuration, CLI flags, API methods, browser mode, migration notes, and common errors. The v4.1.11 source documentation states defaults and failure modes that matter in practice, including node as the default environment, vi.mock's import-only behavior, setup-file caching without isolation, and native add-on trouble in the threads pool. Examples are usually ready to adapt.
Maintenance5/5Version 4.1.11 was current when checked, and the GitHub repository had been pushed on 2026-08-22. The patch release fixed concurrency, browser cleanup, URL encoding, and mock-file access rather than only updating metadata. The repository showed 367 open issues and pull requests, which is a sizable queue, but active releases and near-daily repository movement indicate maintainers are still working through the runner's broad surface.
Ecosystem5/5The npm download API reported 93,033,364 downloads in the checked week, and the repository had 16,996 stars. Vitest integrates directly with Vite configuration and publishes matching packages for coverage, UI, browser execution, and related tooling. That reach makes editor, CI, framework, and reporter support easy to find, though several capabilities appear as optional peers that must stay version-aligned with the core package.

Discussed on

  1. hnVitest Browser Mode Guide72 points
  2. hnJest/Vitest interactive course (runs in the browser)52 points
  3. hnVitest vs. Jest44 points
  4. hnVitest, fast unit-test framework powered by Vite25 points
  5. hnGetting Started with Vitest20 points

Use it if

  • Your application already uses Vite and its tests need the same aliases, transforms, plugins, and TypeScript handling
  • You want Jest-shaped assertions, snapshots, spies, and mocks in an ESM-first codebase
  • Your monorepo needs named test projects with different environments, setup files, or worker settings
  • You need to split a large suite across CI machines with shards and merge their reports afterward
Skip it if

Setup reality

Our clean Node 22 install completed in 9.1 seconds and left 47 packages using 39 MB on disk. npm audit found 0 known vulnerabilities. Vitest declares 20 direct dependencies and 12 peer dependencies; its published archive is 2216 KB unpacked and includes TypeScript declarations. ESM import worked. require() failed under Node.js 22.23.2. A browser-oriented esbuild bundle measured 306 KB minified and 94.4 KB gzipped, so this is tooling to keep out of application code.

A basic vitest.config.ts imports defineConfig from vitest/config. The default environment is node, and global test functions are off unless you enable globals. DOM tests require jsdom or happy-dom, while real-browser runs need a browser provider. Setup files execute inside test workers before each file; globalSetup runs once in the main process. If isolation is disabled, imported modules stay cached even though setup files execute again.

Coverage is another install: choose @vitest/coverage-v8 or @vitest/coverage-istanbul, then run vitest run --coverage. The runner can offer to install the selected provider on first use, which is an awkward surprise in a locked CI job. V8 coverage needs a V8-based runtime and cannot cover Firefox, Bun, or Cloudflare Workers. Istanbul works across more runtimes but instruments source before execution. Set coverage.include if untouched files must appear in the report.

Test files run in parallel workers and isolated environments by default. Concurrent tests use Promise.all and obey maxConcurrency; version 4.1.11 fixes that limit for lifecycle hooks. The default forks pool is the safer choice for native Node add-ons. The docs warn that threads can trigger native crashes and a Node fetch error. vi.mock is hoisted, only sees import-loaded modules, and cannot capture ordinary local variables; use vi.hoisted when a mock factory needs shared state.

Patterns

Write a basic unit test write-unit-test

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

describe('subtotal', () => {
  test('adds line items', () => {
    expect(subtotal([4, 7])).toBe(11)
  })
})

These functions are explicit imports by default. Enable globals only if the shorter syntax is worth adding vitest/globals to your TypeScript types.

Configure files and cleanup configure-test-runner

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    include: ['src/**/*.test.ts'],
    setupFiles: ['./test/setup.ts'],
    restoreMocks: true,
  },
})

setupFiles run inside each test worker. Use globalSetup for one-time services that must start before workers are created.

Assert promise results test-async-result

import { expect, test } from 'vitest'

test('loads one account', async () => {
  await expect(loadAccount('a1')).resolves.toMatchObject({ id: 'a1' })
  await expect(loadAccount('missing')).rejects.toThrow('not found')
})

Await the resolves or rejects chain. Otherwise the test can finish before the assertion settles.

Record calls with vi.fn mock-function

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

test('publishes once', async () => {
  const publish = vi.fn().mockResolvedValue({ id: 'm1' })
  await saveOrder({ publish })
  expect(publish).toHaveBeenCalledOnce()
  expect(publish).toHaveBeenCalledWith('order.saved')
})

Enable clearMocks, mockReset, or restoreMocks deliberately. Each option cleans a different part of mock state.

Replace an imported module mock-es-module

import { expect, test, vi } from 'vitest'
import { readRate } from './rates.js'

vi.mock('./rates.js', () => ({
  readRate: vi.fn().mockResolvedValue(1.25),
}))

test('uses the fixed rate', async () => {
  await expect(readRate('USD')).resolves.toBe(1.25)
})

vi.mock is hoisted and only affects modules loaded through import. It does not intercept require calls.

Give a mock factory shared state share-hoisted-mock-state

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

const mocks = vi.hoisted(() => ({ fetchUser: vi.fn() }))
vi.mock('./users.js', () => ({ fetchUser: mocks.fetchUser }))

test('handles a missing user', async () => {
  mocks.fetchUser.mockResolvedValue(null)
  await expect(showUser('x')).resolves.toBe('missing')
})

Ordinary top-level variables are unavailable to a hoisted vi.mock factory. Create the shared value with vi.hoisted.

Advance a delayed action control-fake-time

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

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

test('flushes after 500 ms', async () => {
  vi.useFakeTimers()
  const flush = vi.fn()
  scheduleFlush(flush)
  await vi.advanceTimersByTimeAsync(500)
  expect(flush).toHaveBeenCalledOnce()
})

Return to real timers after every test. Async timer advancement is safer when callbacks create promises.

Select jsdom for one file test-dom-code

/**
 * @vitest-environment jsdom
 */
import { expect, test } from 'vitest'

test('renders the status', () => {
  document.body.innerHTML = '<p data-status>ready</p>'
  expect(document.querySelector('[data-status]')?.textContent).toBe('ready')
})

Install jsdom separately. A file comment is useful when most of the suite should stay in the faster node environment.

Enforce a coverage floor collect-coverage

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      include: ['src/**/*.ts'],
      thresholds: { lines: 85, branches: 80 },
    },
  },
})

Install @vitest/coverage-v8 at the same version as Vitest, then run vitest run --coverage. include is needed if untouched source files must count.

Run Node and DOM projects split-monorepo-projects

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    projects: [
      { test: { name: 'server', include: ['server/**/*.test.ts'] } },
      { test: { name: 'client', include: ['client/**/*.test.ts'], environment: 'jsdom' } },
    ],
  },
})

Project names should be unique because reporters and CLI filtering use them to identify results. The client project still needs jsdom installed.

Bound concurrent work limit-concurrent-tests

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    maxConcurrency: 4,
  },
})

// In a test file:
test.concurrent('reads account', async ({ expect }) => {
  expect(await readAccount()).toBeTruthy()
})

Concurrent tests run through Promise.all. Use the expect supplied by the test context so snapshots and assertions attach to the correct test.

Split tests across CI jobs shard-ci-suite

# job 1
npx vitest run --shard=1/3 --reporter=blob

# job 2
npx vitest run --shard=2/3 --reporter=blob

# job 3
npx vitest run --shard=3/3 --reporter=blob

Upload every blob report and merge them in a later job. Each shard must use the same Vitest configuration and dependency versions.

Alternatives

PackageRegistryPick it when
jestnpmChoose it for an established Jest suite that depends on its custom runners, transforms, or CommonJS-era behavior.
mochanpmChoose it when you want a smaller, assertion-agnostic Node test runner and prefer assembling the surrounding tools yourself.
uvunpmChoose it for a compact Node suite where single-threaded execution and a narrow API are acceptable.
@web/test-runnernpmChoose it when standards-based browser execution matters more than sharing a Vite-centered unit-test stack.

More testing guides

pytest · chai · jsdom · playwright · coverage · axe-core · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.