12k
All articles

Vitest 5 有哪些变化

了解 Vitest 5 的变化:性能提升、不兼容变更、新默认设置、报告路径,以及升级测试和 CI 的实用清单。

OpenReplay Team
OpenReplay Team
Vitest 5 有哪些变化

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 方法抛出错误移到模块作用域
移除 sequentialAPI 已移除使用 { 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-webdriverio provider 已迁移至 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 升级清单

  1. 将 CI 和本地环境升级到 Node.js 22.12.0+ 和 Vite 6.4.0+。
  2. 运行上文的 grep 命令,在所有遗漏处补上 await。
  3. 将所有 vi.mock 和 vi.hoisted 调用移到所在文件的顶层。
  4. 用 { concurrent: false } 替换 sequential,并为依赖上级目录配置的包目录添加配置文件。
  5. 运行测试套件。如果调用次数断言失败,修复它们,或临时设置 clearMocks: false 作为过渡方案。
  6. 将 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。如果你的测试会以文件形式保存附件,也需要将附件目录一并带入合并任务。

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.