把 Jest 测试迁移到 Vitest,并保留原有行为

来源:Vitest 官方文档「Migrating from Jest」。原文无个人署名,版权归 VoidZero Inc. 与 Vitest contributors。本文为中文翻译整理,补充说明均以“编者注”标明。

Vitest 采用与 Jest 兼容的 API,尽量降低已有测试套件的迁移成本。不过,函数名称相似不代表行为完全相同。迁移时应逐项核对全局 API、模拟状态、模块解析、异步测试与生命周期,再检查筛选表达式和快照。

版本说明:2026 年 10 月 5 日核对时,官网显示 v5.0.3;入门文档要求 Node.js ≥ 22.12.0、Vite ≥ 6.4.0。这里描述的是当日文档,不能据此推定旧版本也具有相同语义。本文只做文档与代码静态核对,没有安装依赖或执行任何测试。

Jest迁移到Vitest的四层检查:全局入口、模拟与模块、异步与钩子、测试名称和快照;逐层核对后再比较测试行为。
编者绘制:按行为分层检查迁移结果,避免只替换 API 名称。

1. 先决定是否启用全局 API

Jest 默认启用全局 API;Vitest 默认不启用。可以通过 globals 配置打开,也可以在每个测试文件中从 vitest 显式导入所用函数。

import { describe, expect, it, vi } from 'vitest'

如果选择保留全局 API 禁用状态,还要注意 testing-library 一类库的自动 DOM 清理可能不会运行。迁移时必须核对项目原来依赖的自动清理机制,不能只以“测试能够执行”作为完成标准。

编者注:如项目使用 React Testing Library,可根据该库所用版本的文档在测试初始化文件显式注册清理;此处不把特定 UI 库的配置当成所有项目的通用配置。

2. mockReset 的含义不同

Jest 的 mock.mockReset() 会把模拟实现替换为空函数,后续调用返回 undefined。Vitest 会恢复创建模拟时的原始实现:对于 vi.fn(impl),重置后又使用 impl。如果旧测试依赖“重置后不再产生原来的返回值或副作用”,迁移时需要重写这部分准备逻辑。

3. mock.mock 的引用会保留

Jest 调用 .mockClear() 时会重新创建模拟状态,因此不能长期缓存旧的 .mock 引用。Vitest 则保留同一个状态引用,以下断言在 Vitest 中成立:

const mock = vi.fn()
const state = mock.mock
mock.mockClear()

expect(state).toBe(mock.mock) // 对照 Jest 的同类写法,这个引用断言会失败

这里讨论的是状态对象的身份,不是说调用记录不会被清除。不要把“引用持续存在”误解为“历史调用持续存在”。

4. 模块模拟工厂必须明确列出导出

原文用一个默认导出的模拟说明差异:Jest 的工厂可以直接返回那个值,而 Vitest 的工厂必须返回对象,并明确提供每个导出字段。

// 原文的 Jest 写法
jest.mock('./some-path', () => 'hello')

// 对应 Vitest 写法
vi.mock('./some-path', () => ({
  default: 'hello',
}))

如果模块还有具名导出,也应在返回对象中对应列出。完整语义以 vi.mock API 为准。

5. 手工模拟目录不会自动生效

与 Jest 不同,Vitest 不会仅因为 <root>/__mocks__ 目录里存在模拟文件就加载它。仍须调用 vi.mock()。如果希望所有测试都使用某个模拟,可把调用放进 setupFiles 指向的初始化文件。

这一步值得独立核查:遗漏模拟声明时,测试可能意外调用真实模块,而不仅仅是少一次断言。编者注:涉及网络、数据库或外部写入的模块,应在隔离环境确认实际加载的是预期实现。

6. 获取真实模块改用异步导入

部分模拟某个包时,Jest 常用 jest.requireActual 取得真实实现;Vitest 对应的是 vi.importActual,调用方需要处理 Promise。

// 原文示意:Jest
const { cloneDeep } = jest.requireActual('lodash/cloneDeep')

// 原文示意:Vitest
const { cloneDeep } = await vi.importActual('lodash/cloneDeep')

编者注:上面保留原文的导出解构示意。具体包的默认导出、具名导出及 CommonJS 互操作形态必须以项目实际模块为准,不能只换函数名而假定 cloneDeep 一定是该子路径的具名导出。

7. 让模拟影响使用它的第三方库

Jest 默认会把相应模块模拟扩展到使用该模块的外部库。Vitest 中,如果某个第三方库内部也需要使用同一模拟,必须明确把该第三方库纳入处理范围。原文给出的配置项是 server.deps.inline:

// 配置项示意,并非完整配置文件
server.deps.inline: ["lib-name"]

应内联的是需要经过处理的第三方库。不要无差别扩大到所有依赖;迁移时应逐个确认被测路径中的模块解析和模拟是否符合预期。

8. 完整测试名称以 > 连接

expect.getState().currentTestName 在两者中的拼接方式不同。Jest 在测试套件名和测试名之间使用空格,Vitest 使用 >,便于区分层级。

- `${describeTitle} ${testTitle}`
+ `${describeTitle} > ${testTitle}`

同一规则也影响 testNamePattern 与命令行 -t。Vitest 会对用 > 连接后的完整名称匹配;跨越套件与测试名的旧表达式必须调整。例如:

vitest -t 'math > adds'

也可只匹配某一段名称,例如 -t adds,或使用能覆盖层级间隔的模式,例如 -t 'math.*adds'。原来的 'math adds' 不应机械保留。

9. 环境变量与工作进程编号

与 Jest 类似,如果调用前没有设置 NODE_ENV,Vitest 会把它设为 test。对应 JEST_WORKER_ID 的变量是 VITEST_POOL_ID,其值始终小于或等于 maxWorkers。如果数据库名称、临时目录或端口分配依赖这个编号,要同步迁移。

Vitest 还暴露 VITEST_WORKER_ID,表示运行中工作进程的唯一 ID。该数字不受 maxWorkers 限制,会随着创建新工作进程而增加。二者含义不同,不应混用。

10. 替换对象属性

原来使用 Jest replaceProperty 修改对象时,可按目标选择 Vitest 的 vi.stubEnv 或 vi.spyOn。前者用于环境变量,后者用于监视或替换对象可支持的成员;它们不是对任意属性的机械一对一替换。迁移时也应保留相应恢复逻辑,避免测试间污染。

11. 将 done 回调改为 Promise 或异步函数

Vitest 不支持以完成回调声明测试。应改为 async/await,或者返回 Promise。原文展示的是把回调中的完成操作改为 Promise 的完成函数;下例按这个意图整理成可独立阅读的写法:

it('should work', () => new Promise<void>((resolve, reject) => {
  doWork((error?: Error) => {
    if (error) {
      reject(error)
      return
    }
    resolve()
  })
}))

编者改写说明:doWork 代表项目已有的回调 API,并非 Vitest API;示例补上了错误拒绝分支。原文只展示 done() 的结构变换。实际测试必须把断言异常或异步错误正确交给 Promise,不能只在成功路径调用完成函数。

12. 核对钩子的返回值和执行顺序

Vitest 的 beforeAll、beforeEach 可以返回用于清理的函数。因此,原来用表达式箭头函数无意间返回其他值的钩子,需要改写为不返回值的块体。原文以 Pinia 为例:

// 迁移前:表达式的值会成为钩子返回值
beforeEach(() => setActivePinia(createTestingPinia()))

// 迁移后:明确不返回该值
beforeEach(() => {
  setActivePinia(createTestingPinia())
})

原文说明 Jest 的钩子顺序采用列表行为,Vitest 默认采用栈行为。若要匹配 Jest,应调整 sequence.hooks:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    sequence: {
      hooks: 'list',
    },
  },
})

编者注:除配置外,仍应检查有依赖关系的 setup/teardown,特别是共享数据库、环境变量及模块状态的清理;不要以顺序改变掩盖测试之间的依赖。

13. TypeScript 类型直接导入

Vitest 没有与 jest 命名空间完全对应的类型入口。原来写成 jest.Mock 的位置,需要直接从 vitest 导入类型:

// Jest
let oldFn: jest.Mock<(name: string) => number>

// Vitest
import type { Mock } from 'vitest'
let fn: Mock<(name: string) => number>

14. 定时器与超时

Vitest 不支持 Jest 的 legacy timers。依赖这套旧定时器实现的测试需要重新核对行为。原来调用 jest.setTimeout 的地方改用 vi.setConfig:

// Jest
jest.setTimeout(5_000)

// Vitest
vi.setConfig({ testTimeout: 5_000 })

这里设置的是测试超时。编者注:迁移时不要通过一味增加超时来掩盖 Promise 未完成、清理未执行或计时器模式不匹配的问题。

15. Vue 快照序列化

这项差异不局限于 Jest 本身。如果之前使用带 vue-cli preset 的 Jest,需要安装 jest-serializer-vue,并在 Vitest 的 snapshotSerializers 中指定它:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    snapshotSerializers: ['jest-serializer-vue'],
  },
})

否则快照里可能出现很多转义后的双引号。应确认变化来自序列化方式,再决定是否接受新的快照;不能在未经查看差异时批量更新。

16. 自定义快照匹配器:实验性接口

原文将本节标注为 experimental,4.1.3+。这是单独的版本边界,不能把它与稳定迁移项混为一谈。Jest 从 jest-snapshot 导入快照组合函数,Vitest 则从 vitest 的 Snapshots 取得:

import { expect, Snapshots } from 'vitest'
const { toMatchSnapshot } = Snapshots

expect.extend({
  toMatchTrimmedSnapshot(received: string, length: number) {
    return toMatchSnapshot.call(this, received.slice(0, length))
  },
})

对应的内联快照写法如下。保留 .call(this, ...),使匹配器使用当前断言上下文。

import { expect, Snapshots } from 'vitest'
const { toMatchInlineSnapshot } = Snapshots

expect.extend({
  toMatchTrimmedInlineSnapshot(
    received: string,
    inlineSnapshot?: string,
  ) {
    return toMatchInlineSnapshot.call(
      this,
      received.slice(0, 10),
      inlineSnapshot,
    )
  },
})

编者注:上述两段为原文示例补上 expect 的显式导入;截取长度与原文保持一致。使用前应继续阅读 快照指南,并核对所安装版本的实验性 API。

迁移如何算完成

完成 API 替换后,还要对照原套件检查:真实模块是否被意外调用、每次测试是否完成 DOM 与状态清理、失败和超时能否准确传播、按名称筛选是否仍命中预期测试、快照差异是否都有合理解释。本文未运行这些检查,也不声称示例或项目已通过测试。静态检查未发现文章代码中含硬编码秘密或主动执行外部输入的逻辑,但这不等于依赖或实际测试环境不存在漏洞。

来源与许可

原文:Migrating from Jest;版本补充:Vitest 官网、Getting Started。正文按 2026 年 10 月 5 日读取的完整迁移章节翻译整理;示例的整理与补充已逐处标注。

Vitest 仓库采用 MIT License,Copyright (c) 2021-Present VoidZero Inc. and Vitest contributors。许可及免责声明全文见文末 MIT 许可。配图为本次原创技术示意图,不是测试运行截图。

Vitest 示例代码 MIT 许可全文

MIT License

Copyright (c) 2021-Present VoidZero Inc. and Vitest contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容