代码 Vitest 5 升级笔记:未 await 的断言开始失败,mock 调用每个测试前被清空

2026-10-20 21:09:38

Vitest 5 升级笔记:未 await 的断言开始失败,mock 调用每个测试前被清空

信源:VoidZero 月度更新(Tales from the Void: September 2026 Recap)中文译文;Vite changelog(2026-10-01,Vite 8.3.2 的 deps: update vitest monorepo to v5)。

Vitest 5 相比 Vitest 4 运行速度最高提升 50%。提速来自三处:跨项目共享转换后的文件、在磁盘缓存模块、减少主进程与 worker 之间的数据传输。同一版还加入了 vitest doctor、--repeats,以及两项会让旧用例开始失败的行为变更——未 await 的异步断言不再假通过,每个测试前会清除 mock 调用。

速度提升的来源

三个机制分别对应不同的瓶颈:

  • 跨项目共享转换结果:monorepo / workspace 配多个 project 时,同一个源文件不会被每个 project 各转换一遍。多 project 场景收益最大,单 project 基本吃不到。
  • 磁盘缓存模块:转换结果落到磁盘,冷启动后第二次跑直接复用。
  • 减少主进程与 worker 之间的数据传输:通信开销随测试文件数增长,文件越多越明显。

两条实践含义(推断,需按自己仓库实测):

  1. CI 的收益通常小于本地热跑。CI 每次都是干净环境,磁盘缓存只有在缓存目录被持久化时才生效;没持久化就等于没开。
  2. 缓存目录要显式对齐到 CI 的 cache 路径。Vitest 的缓存默认落在 node_modules/.vite 一类目录下,不同版本可能调整,升级后建议先确认一次实际路径:
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  // 显式写出来,方便 CI 的 cache 路径与之对齐
  // 默认值以所用版本文档为准
  cache: { dir: 'node_modules/.vite' },
})
# CI 片段:缓存目录不持久化,磁盘缓存等于没开
- uses: actions/cache@v4
  with:
    path: node_modules/.vite
    key: vitest-${{ hashFiles('pnpm-lock.yaml') }}

cache.dir 的字段名与默认值未在本次实测中逐个版本核对,升级前查一次对应版本文档。

vitest doctor:先诊断再改配置

npx vitest doctor

它把一次运行的耗时分解到 setup、import、transform 和测试本身,并会尝试其他配置,给出更快的设置建议。定位「慢在转换还是慢在用例」比凭感觉调 pool、isolate 更快。它是交互式的,适合本地跑;不要塞进 CI 的必过步骤。

行为变更一:未 await 的异步断言会失败

以前忘记 await 的异步断言会假通过——断言返回的 Promise 没人等,失败也就没人看见:

// 假通过:断言没被等待,用例直接算过
expect(fn()).rejects.toThrow()

// Vitest 5 起这类写法会失败
await expect(fn()).rejects.toThrow()

升级后会冒出一批「从来没真正执行过」的失败用例。这是好事,但排查成本集中爆发,建议单独排一次,别和业务改动混在同一个 PR 里。

排查方式:搜 expect( 且同一行没有 await、下一行也没有续接 await 的位置,重点看 .rejects、.resolves 和自定义异步 matcher。

行为变更二:每个测试前清除 mock 调用

等价于自动执行 clearAllMocks。之前手写在 beforeEach 里的 vi.clearAllMocks() 可以删,但要确认语义差别:

  • clear 只清调用记录(mock.calls、mock.results),保留实现。
  • reset 连实现一起清掉,mockImplementation 注册的行为会失效。

依赖跨用例累积调用计数的断言会失败,典型写法:

// 依赖上一个测试留下的调用次数,Vitest 5 下不再成立
test('第二次调用走缓存', () => {
  expect(api.fetch).toHaveBeenCalledTimes(2) // 计数已从 0 重新开始
})

修法是让每个用例自带前置调用,而不是继承上一个用例的状态。

--repeats:把间歇性失败逼出来

vitest run --repeats 10 path/to/test

同一个测试连续跑 N 次,用来暴露 flaky。适合放在 CI 的一个非阻断 job 里跑高风险用例,而不是全量乘 N——耗时是线性增长的。

与 Vite 8.3.x 的关系

Vite 8.3.2 的 chore 里把 vitest monorepo 更新到 v5,说明 Vite 侧的测试依赖已经对齐 v5。仓库里同时锁 Vite 与 Vitest 时,按这个对应关系升,避免出现两套不兼容的版本组合。

迁移检查清单

  • 全局搜未 await 的 expect(...).rejects / .resolves,逐个补 await
  • 确认 beforeEach 里的 vi.clearAllMocks() 可以删;检查是否有依赖 resetAllMocks 语义的地方被误当成 clear
  • 排查依赖跨测试调用计数的断言(toHaveBeenCalledTimes、mock.calls.length),改成用例内自建前置状态
  • 本地跑一次 npx vitest doctor,记录 setup / import / transform / test 四段的耗时基线
  • 多 project 配置的仓库,对比升级前后总耗时,确认共享转换结果确实生效
  • CI 持久化缓存目录,并验证第二次流水线命中缓存
  • 对已知 flaky 用例跑 vitest run --repeats 10,把结果作为升级前的对照
  • Vite 与 Vitest 版本成对升级,对齐到 Vite 8.3.2 / Vitest 5 这一组

未实测的部分

以下结论按机制推断,未在本仓库逐项验证:50% 提速在单 project、无缓存命中的项目上不会全额体现;cache.dir 字段与默认路径在不同版本间可能变化;「每个测试前清除 mock 调用」是否提供配置级 opt-out,需查对应版本文档;vitest doctor 给出的替代配置建议在不同项目结构下的准确度未知。

复制全文 生成海报 前端 测试 Vitest 工具链

推荐文章

程序员茄子在线接单