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 之间的数据传输:通信开销随测试文件数增长,文件越多越明显。
两条实践含义(推断,需按自己仓库实测):
- CI 的收益通常小于本地热跑。CI 每次都是干净环境,磁盘缓存只有在缓存目录被持久化时才生效;没持久化就等于没开。
- 缓存目录要显式对齐到 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 给出的替代配置建议在不同项目结构下的准确度未知。