12k
All articles

What Changed in Vitest 5

See what changed in Vitest 5, including performance gains, breaking changes, new defaults, reporter paths, and a practical checklist for upgrading your tests and CI.

OpenReplay Team
OpenReplay Team
What Changed in Vitest 5

Vitest 5.0, released on September 3, 2026, is a performance-focused major that requires Node.js 22.12.0+ and Vite 6.4.0+, turns on mock clearing by default, fails unawaited async assertions, and moves reporter output under a single .vitest/ directory.

The failures after the upgrade are mostly easy. The hard ones are the tests that pass locally on Vitest 4 and go red in CI with an error that says nothing about why.

This article sorts the v5.0.0 release notes by impact. First, what actually got faster. Then the changes that need code edits, the changes that quietly move paths and matching rules, an upgrade checklist, and a verdict.

Key Takeaways

  • Vitest 5.0 requires Node.js 22.12.0 or later and Vite 6.4.0 or later.
  • The Vitest team’s benchmarks show most tested setups running 8 to 25% faster, and up to 53% faster in some VM-pool setups. Setups dominated by environment setup barely change.
  • clearMocks now defaults to true, so an assertion on call history recorded in a setup file, a beforeAll hook or an earlier test now sees zero calls.
  • Blob reports, attachments, and JSON, JUnit and HTML reporter output now default to paths under .vitest/, so CI artifact steps need updating.
  • toThrow('') now matches any thrown error, so an assertion meant to check for an empty message needs an explicit pattern.

Why Does Vitest 5 Run Faster?

Most setups in the Vitest team’s own benchmarks run 8 to 25% faster, according to the Vitest 5 announcement. VM pools gain the most, with up to 53% in some setups. Not every setup speeds up: runs where creating the test environment takes most of the time, such as forks with jsdom and isolation, stay within 3% of Vitest 4.1. The numbers come from vitest-dev/benchmarks, where the team generated test apps of different sizes, from a small 5-file package up to a 1,280-module monolith. In VoidZero’s launch post, this is rounded to “vm pools up to 53% faster, ~18% boost across the board including Browser Mode”.

According to the release notes, four changes produce most of the gain:

  • Shared Vite server. Inline projects now share one Vite server instead of each starting its own.
  • fsModuleCache. This is now a top-level option. It saves transformed modules to disk, so a rerun or another Vitest process can skip that work.
  • Fewer round trips. Modules that are already transformed now reach a worker in one trip from the main process.
  • VM pool reuse. The vmThreads and vmForks pools share compiled code between contexts and load the module graph ahead of time.

Vitest also supports Node’s on-disk compile cache, but it is opt-in.

Which Vitest 5 Changes Need a Code Edit?

Six Vitest 5 changes cause a test failure or a config error on the first run. The migration guide covers each one.

ChangeFirst-run symptomFix
clearMocks: true by defaultCall-count assertions see 0Trigger calls inside the asserting test, or set clearMocks: false
Unawaited async assertionTest failsAdd await
Hoisted vi call outside top levelThrowsMove it to module scope
sequential removedRemoved API{ concurrent: false }
No parent-directory config lookupConfig not foundAdd a config to the package folder
Bench API rewrittenOld bench code breaksMove to the fixture model

Mock Clearing and Hoisting

In Vitest 5, clearMocks defaults to true, so vi.clearAllMocks() runs before every test. Call history recorded in a setup file, a beforeAll hook or an earlier test is gone before the next test asserts on it. Mock implementations are left in place.

// Vitest 5.0.x
const track = vi.fn()
beforeAll(() => initAnalytics(track))

it('tracks once on init', () => {
  expect(track).toHaveBeenCalledTimes(1) // now receives 0 calls
})

The fix is to trigger the call inside the test that asserts on it. Setting clearMocks: false in test config restores the old behaviour while you audit the suite.

Calling vi.mock, or any other hoisted vi call, anywhere except the top level of a file now throws. Vitest hoists these calls to the top of the module anyway, so code inside a describe block never ran where it was written.

// Vitest 5.0.x: throws
describe('UserCard', () => {
  const fetchUser = vi.fn()
  vi.mock('./api', () => ({ fetchUser }))
})

// Vitest 5.0.x: works
const { fetchUser } = vi.hoisted(() => ({ fetchUser: vi.fn() }))
vi.mock('./api', () => ({ fetchUser }))

describe('UserCard', () => {
  it('renders the user', async () => {
    fetchUser.mockResolvedValue({ name: 'Ada' })
    // mount and assert
  })
})

If your Vue suites mock their API layer, the same top-level pattern applies to mocking API calls in Vue tests with Vitest.

Unawaited Assertions

A test that leaves an async assertion unawaited now fails. A missing await before expect(...).resolves or .rejects turns the test red.

// Vitest 5.0.x
test('loads config', async () => {
  expect(loadConfig()).resolves.toEqual({ ok: true }) // fails
  await expect(loadConfig()).resolves.toEqual({ ok: true }) // passes
})

This grep lists candidates. You still need to check each hit for a leading await:

grep -rnE "expect\(.*\)\.(resolves|rejects)" src/ --include='*.test.*' --include='*.spec.*'

Concurrency, Config Lookup and Bench

The sequential options on tests and suites are gone. To opt out of concurrency, use test('example', { concurrent: false }, ...) or describe('suite', { concurrent: false }, ...).

Vitest 5 stops looking in folders above the current one for a config file. If you run vitest from a package subfolder, that folder needs its own config.

The bench API was rewritten. You no longer import bench at the top of a file. Instead, you take it from the test context inside an ordinary test() call in a benchmark file.

The release notes list other breaking items to check if they apply to you:

  • expect.poll now fails when it times out.
  • Deprecated entry points were removed.
  • @vitest/runner is deprecated, and vitest no longer depends on @vitest/expect because the assertion code now ships inside vitest itself.
  • The @vitest/browser-webdriverio provider moved to the vitest-community organization and is now maintained by the community.
  • workerId is now 1-based.

toThrow('') now matches any thrown error. If you really want to check for an empty message, pass a regex such as /^$/ instead.

What Changes Silently in Vitest 5?

Six Vitest 5 changes don’t throw an error. Instead, a path, a filter or a match result changes underneath your existing setup.

  • Output paths. Blob reports and --merge-reports default to .vitest/blob/. Attachments move from .vitest-attachements/ to .vitest/attachments/. JSON, JUnit and HTML reporter files also default to .vitest.
  • -t filters. The separator for test-name filters is now >. Check any CI scripts that filter by suite path.
  • Browser locators. locators.exact is now on by default in Browser Mode.
  • Text matching. toHaveTextContent is now strict. toMatchTextContent is the new alternative.
  • Coverage globs. include and exclude patterns now match each file’s path relative to the project root, and a pattern with no wildcard counts as a whole folder. The set of files counted toward coverage can change, so check your thresholds after the first run.
  • Inline projects. Inline projects now inherit the root config as if extends: true were set.

A typical artifact step changes like this:

-          path: .vitest-attachements/
+          path: .vitest/attachments/
+          # sharded runs: upload .vitest/blob/ for --merge-reports

New in Vitest 5 and Worth Knowing

vi.when lets you give a spy a different result for each set of arguments. calledWith accepts asymmetric matchers, and calls whose arguments match nothing fall through to the original implementation.

// Vitest 5.0.x
vi.when(getUser).calledWith(1).thenResolve({ id: 1, name: 'Ada' })

In Browser Mode, setting test.browser.traceView: true turns on the trace view. Each interaction, assertion and page.mark call is saved as a DOM snapshot, so you can replay the test one step at a time in the UI.

Nested projects are now supported, which helps monorepos group related projects.

Vitest 5 Upgrade Checklist

  1. Move CI and local environments to Node.js 22.12.0+ and Vite 6.4.0+.
  2. Run the grep above and add await wherever it’s missing.
  3. Move every vi.mock and vi.hoisted call to the top level of its file.
  4. Replace sequential with { concurrent: false }, and add configs to package folders that relied on a parent config.
  5. Run the suite. If call-count assertions fail, fix them or set clearMocks: false as a temporary bridge.
  6. Update CI artifact paths to .vitest/, and review -t filters and coverage thresholds.

Should You Upgrade to Vitest 5 Now or Wait?

Upgrade to Vitest 5 this sprint if your CI already runs Node.js 22.12.0+ and Vite 6.4.0+. Most of the required edits are mechanical.

The exception is a suite that asserts on mock call history carried across tests, whether from setup files, beforeAll hooks, or one test relying on another test’s calls. Those failures give no hint of the cause, so audit those assertions first and then upgrade. Component suites should also re-run their text and locator assertions; the patterns in testing Svelte 5 components with Vitest show where they tend to appear.

Vitest 5 is faster, and most of what it breaks is test code that was already wrong. Start with the grep and the vi.mock move on a branch, check the CI artifact paths, and let the first CI run point you to the rest.

FAQs

Does Vitest 5 also turn on mockReset or restoreMocks by default?

No. The Vitest 5 migration guide changes the default only for clearMocks. clearMocks calls vi.clearAllMocks() before each test and resets mock.calls, mock.instances, mock.contexts and mock.results, but keeps implementations. mockReset goes further. It clears history and resets each implementation to its original, so a mock created with vi.fn(impl) goes back to impl. restoreMocks puts back the original implementations of spies created with vi.spyOn.

Why does my -t filter match fewer tests after upgrading to Vitest 5?

In Vitest 5, testNamePattern (the -t flag) is checked against the full test name, built by putting ' > ' between each suite name and the test name. That is the same text you see in the reporter output. Vitest 4 used a single space between the parts, like Jest. A pattern only breaks if it crosses from one part of the name into the next. To fix it, match just one part, such as -t adds, or put a wildcard between the parts, such as -t 'math.*adds'.

Why can't Vitest 5 resolve vite after upgrading with Yarn?

In Vitest 5, vite moved from a direct dependency to a required peer dependency, so Vitest runs on whatever Vite version your project installs. npm, pnpm, Bun and Deno add peer dependencies for you. Yarn leaves that step to you. Add vite at version 6.4.0 or later to your package.json and reinstall, and Vitest can resolve it again.

How do I merge sharded test reports in Vitest 5?

Run each shard with the blob reporter, for example vitest run --reporter=blob --shard=1/3 on the first machine. Each shard writes its results to .vitest/blob/ by default, and the --outputFile.blob flag changes that location. Copy the directory from every machine into one final job and run vitest --merge-reports. If your tests save attachments as files, bring the attachments folder into the merge job as well.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.