Skip to content
Nilay Kabariya

Entry No.043·Tested··4 min read

Vitest 5 migration: breaking changes, tested

I upgraded a passing Vitest 4.1.11 suite to 5.0.3. Six changes broke it, from cleared mocks to renamed snapshots, each with the error and the fix.

by Nilay#vitest#testing#nodeTESTED

Verdict

Six behaviours that passed on Vitest 4.1.11 failed or changed on 5.0.3, each with a clear message and a one-line fix. The sneaky one is test.each titles: snapshot keys change, and locally Vitest 5 quietly writes new snapshots instead of failing.

Tested on

Vitest
4.1.11 → 5.0.3
Vite
8.3.3
Node
24.14.0
OS
Windows 11 Pro
Contents
  1. Before you upgrade01
  2. What broke, at a glance02
  3. 1. Mocks are cleared before every test03
  4. 2. Unawaited async assertions fail the test04
  5. 3. vi.mock inside a function fails the whole file05
  6. 4. Vitest no longer finds a config in a parent folder06
  7. 5. test.each titles changed, and so did snapshot keys07
  8. 6. expect.poll fails when the function is too slow08
  9. In the release notes, not covered by my tests09
  10. How this was tested10

Vitest 5.0 came out on September 3, 2026, and 5.0.3 is current. Its release notes list dozens of breaking changes, most of them in browser mode and the reporter API. To find the ones that break an ordinary test suite, I wrote small tests for the riskiest changes, confirmed every one passed on Vitest 4.1.11, then upgraded to 5.0.3 and ran the same files.

Before you upgrade

Vitest 5 needs Node ^22.12.0 || ^24.0.0 || >=26.0.0 and Vite 6.4 or newer (its engines and peerDependencies). On Node 20 or Vite 5, upgrade those first.

What broke, at a glance

Change On Vitest 4.1.11 On Vitest 5.0.3
Mocks cleared before each test call counts carried over expected "vi.fn()" to be called 2 times, but got 1 times
Unawaited async assertions passed Promise returned by expect(actual).resolves.toBe(expected) was not awaited
vi.mock inside a function passed, with a warning whole file fails: 1 call … was defined outside of the module's top level scope
Config in a parent folder found not found: ReferenceError: test is not defined
test.each titles with $name greets 'alice' greets alice, so snapshot keys change
expect.poll past its timeout waited 3 s and passed expect.poll() function didn't resolve in time.

And one that goes the other way: expect(fn).toThrow('') failed on 4.1.11 (expected [Function] to throw error matching /^$/ but got 'boom') and passes on 5.0.3, which reverted that behaviour.

1. Mocks are cleared before every test

A spy defined once at the top of a file used to keep counting across tests. Vitest 5 clears it before each one:

const spy = vi.fn();
test('first call', () => { spy('a'); expect(spy).toHaveBeenCalledTimes(1); });
test('counts calls across tests', () => { spy('b'); expect(spy).toHaveBeenCalledTimes(2); });
AssertionError: expected "vi.fn()" to be called 2 times, but got 1 times

The new default is usually what you wanted. A test that depends on calls from an earlier test is order-dependent. Fix the test to set up its own calls. To restore the old behaviour while you migrate:

export default defineConfig({ test: { clearMocks: false } });

2. Unawaited async assertions fail the test

expect(promise).resolves… and .rejects… return a promise. Without await, Vitest 4 let the test finish before the assertion ran. Vitest 5 fails it:

Error: Promise returned by `expect(actual).resolves.toBe(expected)` was not awaited.
This assertion is asynchronous and must be awaited; otherwise, it is not guaranteed
to complete before the test finishes

Make the test async and await the assertion:

test('resolves', async () => { await expect(Promise.resolve(1)).resolves.toBe(1); });

These failures are worth fixing rather than silencing: an unawaited assertion could never fail your test, even when the value was wrong.

3. vi.mock inside a function fails the whole file

Vitest 4 printed a warning and ran the file anyway:

Warning: A vi.mock('node:fs') call in "test/hoist.test.js" is not at the top level of the module.
… This will become an error in a future version.

On 5.0.3 it’s that error, and no test in the file runs:

Error: 1 call in "test/hoist2.test.js" was defined outside of the module's top level scope:

- vi.mock('node:path') at test/hoist2.test.js:2:20

Although it appears nested, it will be hoisted and executed before anything in this file.
Move it to the top level to reflect its actual execution order.

Move vi.mock (and vi.hoisted) to the top level of the file. It was always hoisted there anyway; the error just makes the code say so.

vi.mock('node:fs', () => ({ readFileSync: () => 'mocked' }));

If you upgrade from a version that printed that warning, search your test output for is not at the top level first. Those are exactly the files Vitest 5 will refuse.

4. Vitest no longer finds a config in a parent folder

Running Vitest from a subfolder used to pick up the project’s vitest.config.js from above. Vitest 5 only looks in the folder you run it from. My root config set globals: true, so the test in sub/ lost test itself:

ReferenceError: test is not defined

It can show up as any missing setting: globals, aliases, setup files, environment. Monorepos where a package runs vitest without its own config are the usual victims.

Point it at the config, or run from the root:

npx vitest run --config ../vitest.config.js

5. test.each titles changed, and so did snapshot keys

String values in a $name title are no longer quoted:

Vitest 4.1.11:  ✓ greets 'alice'
Vitest 5.0.3:   ✓ greets alice

That breaks -t "greets 'alice'" filters, and it renames every snapshot written from such a test. Here’s what happened to a snapshot file written by Vitest 4:

  • On your machine, Vitest 5 writes a new snapshot under the new name, marks the old one obsolete, and passes. The new snapshot was never compared with anything.
  • In CI (CI=true), nothing is written, and the run fails:
Error: Obsolete snapshots found when no snapshot update is expected.
Error: Snapshot `greets alice 1` mismatched

After upgrading, update snapshots once, locally, and review the diff:

npx vitest run -u

Check the diff before committing: every renamed key should only be a renamed key, with the same content.

6. expect.poll fails when the function is too slow

expect.poll retries until its timeout. On Vitest 4, a callback that was still pending at the timeout was simply waited for. Mine resolved after 3 seconds against a 500 ms timeout, and the test passed in 3 s. On 5.0.3 it stops at the timeout:

expect.poll() function didn't resolve in time.

Raise timeout to what the call really needs, or make each call faster. The poll is meant to retry a quick check many times, not wait on one slow one.

In the release notes, not covered by my tests

These are listed as breaking in the 5.0.0 notes, but I didn’t reproduce them: json and junit reporters write to .vitest/ by default, -t uses > between suite and test names, the sequential option is removed in favour of concurrent (my { sequential: true } describe still ran without an error), browser locators changed, and the benchmark API was rewritten. If you use any of them, read the release notes for those items.

How this was tested

One project on Windows 11 with Node 24.14.0 and Vite 8.3.3. Each test file was written to pass on Vitest 4.1.11, and did (all except the toThrow('') case above). Then Vitest was upgraded to 5.0.3 on a clean node_modules and the same files were run unchanged. Each fix was applied and run again. The snapshot case used a snapshot file written by 4.1.11 and was run both normally and with CI=true. Error text is copied from the runs.

— N.K., end of entry No.043

Useful? Pass it on:Post on XFollow @EmotionalMatter

Related entries

  1. No.039

    npm 12 breaking changes, tested: what actually breaks

    The headline change, install scripts blocked by default, broke puppeteer, cypress, sqlite3 and Claude Code's npm package, and silently skipped lefthook's git hooks. esbuild, bcrypt, sharp and better-sqlite3 kept working. Every install reported success, and the only sign was a warning after the summary.

    TESTED5 min
  2. No.031

    TypeScript 7 migration guide: breaking changes, tested

    Every option TypeScript 6.0.3 marked as deprecated was a hard error on 7.0.2, and ignoreDeprecations no longer silences it. Two defaults that arrived in 6.0 (strict on, @types not loaded automatically) also break projects coming straight from 5.9.

    TESTED6 min
  3. No.020

    Upgrading to TypeScript 7? 9 tools tested (ESLint, Jest, ts-node)

    Plain TypeScript 7 broke ts-node, ts-jest, typescript-eslint, TypeDoc and ts-loader, and npm refused to install it at all without a flag. Microsoft's side-by-side setup ran all 9 tools with no errors while tsc stayed on 7.

    TESTED6 min

Post card · Newsletter

Get the next fix in your inbox.

One short email when a new entry is published. No spam, never shared, and you can leave any time.

— Nilay

or follow by RSSor on X

By subscribing you agree to the privacy note. One click to leave.

tip: paste the exact error text