Vitest 5.0 于 2026 年 9 月 3 日发布,是一个以性能为核心的大版本。它要求 Node.js 22.12.0+ 和 Vite 6.4.0+,默认开启 mock 清理,未 await 的异步断言会导致测试失败,并将报告器输出统一归入 .vitest/ 目录。
升级后的大多数失败都很好处理。真正棘手的是那些在本地 Vitest 4 上能通过、到了 CI 却变红的测试,而且报错信息完全看不出原因。
本文按影响程度梳理 v5.0.0 发布说明:先讲究竟哪些地方变快了,再介绍需要修改代码的变更、悄然改变路径和匹配规则的变更,最后给出升级清单和结论。
核心要点
- Vitest 5.0 要求 Node.js 22.12.0 或更高版本,以及 Vite 6.4.0 或更高版本。
- Vitest 团队的基准测试显示,大多数被测配置提速 8% 到 25%,部分 VM 池配置最高提速 53%。以环境初始化为主要耗时的配置几乎没有变化。
clearMocks现在默认为true,因此对 setup 文件、beforeAll钩子或前一个测试中记录的调用历史进行断言时,看到的调用次数将为零。- blob 报告、附件以及 JSON、JUnit 和 HTML 报告器的输出现在默认位于
.vitest/下,CI 中的产物(artifact)步骤需要相应更新。 toThrow('')现在会匹配任何抛出的错误,因此如果要断言错误信息为空,需要显式传入匹配模式。
为什么 Vitest 5 运行更快?
根据 Vitest 5 发布公告,在 Vitest 团队自己的基准测试中,大多数配置提速 8% 到 25%。VM 池收益最大,部分配置最高可提速 53%。但并非所有配置都会变快:对于创建测试环境占大部分耗时的场景,例如启用隔离的 forks + jsdom,与 Vitest 4.1 的差距在 3% 以内。这些数据来自 vitest-dev/benchmarks,团队在其中生成了不同规模的测试应用,从 5 个文件的小型包到包含 1,280 个模块的单体应用不等。VoidZero 的发布推文将其概括为“vm pools up to 53% faster, ~18% boost across the board including Browser Mode”(VM 池最高提速 53%,包括 Browser Mode 在内整体提升约 18%)。
根据发布说明,大部分性能提升来自以下四项改动:
- 共享 Vite 服务器。 内联项目(inline projects)现在共享同一个 Vite 服务器,而不再各自启动一个。
fsModuleCache。 现已成为顶层配置项。它会将转换后的模块保存到磁盘,重新运行或其他 Vitest 进程可以跳过这部分工作。- 减少往返通信。 已经转换过的模块现在只需从主进程一次传输即可到达 worker。
- VM 池复用。
vmThreads和vmForks池会在不同上下文之间共享已编译代码,并提前加载模块图。
Vitest 还支持 Node 的磁盘编译缓存,但需要手动开启。
哪些 Vitest 5 变更需要修改代码?
有六项 Vitest 5 变更会在首次运行时导致测试失败或配置错误。迁移指南对每一项都有说明。
| 变更 | 首次运行时的症状 | 修复方式 |
|---|---|---|
clearMocks: true 成为默认值 | 调用次数断言结果为 0 | 在执行断言的测试内部触发调用,或设置 clearMocks: false |
| 未 await 的异步断言 | 测试失败 | 添加 await |
在顶层之外调用会被提升的 vi 方法 | 抛出错误 | 移到模块作用域 |
移除 sequential | API 已移除 | 使用 { concurrent: false } |
| 不再向上级目录查找配置 | 找不到配置 | 在包目录中添加配置文件 |
| Bench API 重写 | 旧的 bench 代码失效 | 迁移到 fixture 模型 |
Mock 清理与提升
在 Vitest 5 中,clearMocks 默认为 true,因此每个测试运行前都会执行 vi.clearAllMocks()。在 setup 文件、beforeAll 钩子或前一个测试中记录的调用历史,会在下一个测试进行断言之前被清除。mock 的实现则会保留。
// Vitest 5.0.x
const track = vi.fn()
beforeAll(() => initAnalytics(track))
it('tracks once on init', () => {
expect(track).toHaveBeenCalledTimes(1) // now receives 0 calls
})
修复方法是在执行断言的测试内部触发调用。在排查整个测试套件期间,可以在 test 配置中设置 clearMocks: false 恢复旧行为。
现在,在文件顶层以外的任何位置调用 vi.mock 或其他会被提升(hoisted)的 vi 方法都会抛出错误。反正 Vitest 都会把这些调用提升到模块顶部,所以写在 describe 块里的代码本来就从未在其书写位置执行过。
// 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
})
})
如果你的 Vue 测试套件会 mock API 层,同样的顶层写法也适用于在 Vue 测试中使用 Vitest mock API 调用。
未 await 的断言
如果测试中存在未 await 的异步断言,测试现在会失败。在 expect(...).resolves 或 .rejects 前漏写 await 会导致测试变红。
// Vitest 5.0.x
test('loads config', async () => {
expect(loadConfig()).resolves.toEqual({ ok: true }) // fails
await expect(loadConfig()).resolves.toEqual({ ok: true }) // passes
})
下面的 grep 命令可以列出可疑位置,但你仍需逐一检查每处匹配前是否有 await:
grep -rnE "expect\(.*\)\.(resolves|rejects)" src/ --include='*.test.*' --include='*.spec.*'
并发、配置查找与 Bench
测试和测试套件上的 sequential 选项已被移除。如需禁用并发,请使用 test('example', { concurrent: false }, ...) 或 describe('suite', { concurrent: false }, ...)。
Vitest 5 不再向当前目录的上级目录查找配置文件。如果你在某个包的子目录中运行 vitest,该目录需要有自己的配置文件。
Bench API 已被重写。不再需要在文件顶部导入 bench,而是在基准测试文件中的普通 test() 调用里,从测试上下文中获取它。
发布说明还列出了其他破坏性变更,请检查是否与你相关:
expect.poll超时时现在会失败。- 已弃用的入口点已被移除。
@vitest/runner已弃用;vitest不再依赖@vitest/expect,因为断言代码现在直接内置于vitest中。@vitest/browser-webdriverioprovider 已迁移至 vitest-community 组织,现由社区维护。workerId现在从 1 开始计数。
toThrow('') 现在会匹配任何抛出的错误。如果你确实想断言错误信息为空,请改为传入正则表达式,例如 /^$/。
Vitest 5 中有哪些“静默”变化?
有六项 Vitest 5 变更不会抛出错误,而是在你现有配置的底层悄悄改变路径、过滤规则或匹配结果。
- 输出路径。 blob 报告和
--merge-reports默认使用.vitest/blob/。附件从.vitest-attachements/移至.vitest/attachments/。JSON、JUnit 和 HTML 报告器的文件默认也输出到.vitest。 -t过滤器。 测试名称过滤器的分隔符现在是>。请检查所有按套件路径过滤的 CI 脚本。- 浏览器定位器。 在 Browser Mode 中,
locators.exact现在默认开启。 - 文本匹配。
toHaveTextContent现在采用严格匹配,新增的toMatchTextContent可作为替代。 - 覆盖率 glob。
include和exclude模式现在匹配的是每个文件相对于项目根目录的路径,不含通配符的模式会被视为整个目录。计入覆盖率的文件集合可能因此发生变化,首次运行后请检查覆盖率阈值。 - 内联项目。 内联项目现在会继承根配置,效果等同于设置了
extends: true。
一个典型的产物步骤修改如下:
- path: .vitest-attachements/
+ path: .vitest/attachments/
+ # sharded runs: upload .vitest/blob/ for --merge-reports
Vitest 5 值得关注的新特性
vi.when 可以为 spy 针对不同参数组合返回不同结果。calledWith 支持非对称匹配器(asymmetric matchers),参数未匹配任何规则的调用会回落到原始实现。
// Vitest 5.0.x
vi.when(getUser).calledWith(1).thenResolve({ id: 1, name: 'Ada' })
在 Browser Mode 中,设置 test.browser.traceView: true 即可开启追踪视图(trace view)。每一次交互、断言以及 page.mark 调用都会保存为 DOM 快照,你可以在 UI 中逐步回放测试。
现在支持嵌套项目,便于在 monorepo 中对相关项目进行分组。
Vitest 5 升级清单
- 将 CI 和本地环境升级到 Node.js 22.12.0+ 和 Vite 6.4.0+。
- 运行上文的 grep 命令,在所有遗漏处补上
await。 - 将所有
vi.mock和vi.hoisted调用移到所在文件的顶层。 - 用
{ concurrent: false }替换sequential,并为依赖上级目录配置的包目录添加配置文件。 - 运行测试套件。如果调用次数断言失败,修复它们,或临时设置
clearMocks: false作为过渡方案。 - 将 CI 产物路径更新为
.vitest/,并检查-t过滤器和覆盖率阈值。
现在就升级 Vitest 5,还是再等等?
如果你的 CI 已经运行在 Node.js 22.12.0+ 和 Vite 6.4.0+ 上,建议在本迭代内升级到 Vitest 5。大部分必要的修改都是机械性的。
例外情况是:测试套件中存在跨测试依赖 mock 调用历史的断言,无论这些调用来自 setup 文件、beforeAll 钩子,还是一个测试依赖另一个测试产生的调用。这类失败完全不会提示原因,因此应先排查这些断言,再进行升级。组件测试套件也应重新运行文本和定位器相关的断言;使用 Vitest 测试 Svelte 5 组件一文中的写法展示了这类断言通常出现在哪里。
Vitest 5 更快了,而它破坏的大多是本身就有问题的测试代码。建议先在分支上执行 grep 检查并迁移 vi.mock,核对 CI 产物路径,然后让第一次 CI 运行为你指出剩下的问题。
常见问题
Vitest 5 是否也默认开启了 mockReset 或 restoreMocks?
没有。Vitest 5 迁移指南只修改了 clearMocks 的默认值。clearMocks 会在每个测试之前调用 vi.clearAllMocks(),重置 mock.calls、mock.instances、mock.contexts 和 mock.results,但会保留 mock 的实现。mockReset 则更进一步:它会清除调用历史,并将每个实现重置为初始实现,因此通过 vi.fn(impl) 创建的 mock 会恢复为 impl。restoreMocks 则会还原通过 vi.spyOn 创建的 spy 所对应的原始实现。
为什么升级到 Vitest 5 后,我的 -t 过滤器匹配到的测试变少了?
在 Vitest 5 中,testNamePattern(即 -t 参数)会与完整的测试名称进行匹配,完整名称由各级套件名和测试名以 ' > ' 连接而成,与报告器输出中显示的文本一致。Vitest 4 与 Jest 一样,使用单个空格连接各部分。只有当匹配模式跨越名称中的不同部分时才会失效。修复方法是只匹配其中一个部分,例如 -t adds,或在各部分之间使用通配符,例如 -t 'math.*adds'。
使用 Yarn 升级后,为什么 Vitest 5 无法解析 vite?
在 Vitest 5 中,vite 从直接依赖改为必需的 peer dependency,因此 Vitest 会使用你项目中安装的 Vite 版本。npm、pnpm、Bun 和 Deno 会自动安装 peer dependency,而 Yarn 需要你手动完成这一步。在 package.json 中添加 6.4.0 或更高版本的 vite 并重新安装依赖,Vitest 即可正常解析。
在 Vitest 5 中如何合并分片测试报告?
使用 blob 报告器运行每个分片,例如在第一台机器上执行 vitest run --reporter=blob --shard=1/3。每个分片默认将结果写入 .vitest/blob/,可通过 --outputFile.blob 参数修改该位置。将每台机器上的该目录复制到最终的汇总任务中,然后运行 vitest --merge-reports。如果你的测试会以文件形式保存附件,也需要将附件目录一并带入合并任务。
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