两条街的转角,开着一家老牌饭店和一家新派餐厅。老牌饭店叫 J 记,经营了十几年,菜单厚得能当书读,熟客都说它稳;新派餐厅叫 V 家,背靠最新供应链体系,食材从产地直送,点完菜十分钟就能上桌。你是一名天天要给代码“试吃”的开发者,而这两家餐厅,就是前端测试界最出名的两个“后厨”——Jest 和 Vitest。本文会带你参观两家的后厨:从备菜(安装配置)、点菜(API 写法)、拦截供应商(Mock/Spy)、拍照存档(快照)、试菜完整度(覆盖率)到搬家成本(迁移),一次看清该请哪家厨房帮你把关代码质量。
给前端项目搭测试,是 AI 应用工程质量的地基。学会对比 Jest 与 Vitest,你就能在团队里拍板“测试框架选谁”,避免项目后期推倒重来。
可视化演示
展示快速排序以末尾元素为基准的分区过程,小于基准的元素左移,大于基准的元素右移。
本节配有交互动效(进入视口后自动加载,约 3KB)。
快速排序 — 初始状态 数组共9个元素,选择末尾元素6作为基准 pivot=arr[8]=6 即将开始分区操作
框架概览与选型
街角开着两家餐厅。J 记是经营十几年的老牌饭店,菜单厚得能当书读,熟客认的就是这份稳定;V 家是今年新开的现代餐厅,背靠最新供应链,食材从产地直送,点完菜十分钟就能上桌。你要为项目选一个“测试后厨”,就得先把两家餐厅的门面看个明白。
现实世界里,Jest 是 Facebook 于 2014 年开源的老牌测试框架,社区庞大、插件齐全,几乎所有前端项目都见过它;Vitest 则是 2021 年底由 Vite 团队孵化的新秀,主打与 Vite 共享构建管道,把“快”写进了基因。两者都能完成单元测试、组件测试和快照测试,但设计理念分道扬镳。
选型主要看四个维度:生态完整度、运行性能、与现有技术栈的兼容度、迁移成本。老项目已深度依赖 jest-dom、eslint-plugin-jest 等周边库时,留在 Jest 更稳;新项目若底层是 Vite,上 Vitest 几乎零配置接入。一句话:项目用 Vite 就选 Vitest,存量 Jest 生态别硬拆。
所以选餐厅要看老板自己的供应链。食材已标准化直供(Vite 项目),V 家后厨天然合拍;J 记老后厨运营多年、伙计顺手,强行搬家反而折腾。用上面的评分公式给两家打打分,别只看门面,理性决策。
- 生态权重 0.4表示插件、社区教程、周边工具在选型中的分量,Jest 在这项明显占优。
- 性能权重 0.3表示测试运行速度的权重,Vitest 凭借原生 ESM 在这项领先。
- 兼容权重 0.2表示与现有构建链的协同程度,Vite 项目选 Vitest 此项接近满分。
- 迁移权重 0.1表示从现有框架切换的成本,权重最低,因为两框架 API 大多通用。
安装与配置
决定了去哪家后厨,下一步就是置办家伙。J 记的招聘启事写着:老伙计请备好“装修队”——因为它的中央厨房需要自己搭传送带,安装 jest 的同时还得配 babel-jest、@types/jest 一整套;V 家的启事很简单:只要你的项目是 Vite 体系,装一个 vitest 包就能开工。
实际安装上,Jest 通常需要 jest、babel-jest、@types/jest 三个包,处理 TypeScript 还要加 ts-jest;Vitest 只需要 vitest 一个包,若项目已有 vite.config,在 test 字段里补充环境即可。依赖数量差三倍,这也是很多新项目直接选 Vitest 的原因。
配置文件的差异同样明显:Jest 用 jest.config.js 声明 transform、testEnvironment、setupFilesAfterEnv;Vitest 复用 vite.config.ts,在 defineConfig 里追加 test: { environment: 'jsdom', globals: true }。这意味着 Vitest 少维护一套配置体系,开发构建与测试共用同一份模块解析规则,配置漂移的概率大幅降低。
所以 V 家能比 J 家早开火,关键不在炒菜(跑断言)快,而在省掉了排队过传送带(模块转换)的时间。老后厨的传送带是历史包袱,新后厨直接共享 Vite 的流水线,自然更轻快。
- 配置加载时间框架读取配置文件并加载依赖脚本的固定开销,两者差别不大。
- 模块转换总耗时每个源码模块都要被转换工具处理,Jest 每个模块都过 Babel,Vitest 只转必要部分。
- 用例执行总耗时真正的断言执行时间,两个框架在此项差异不大。
测试API写法
进了后厨,点菜方式大同小异。J 记和 V 家的菜单长得几乎一模一样:describe 是“菜单分类”,it 或 test 是“一道菜”,expect 是“夹一筷子尝味道”。在 J 记点过菜的客人,到 V 家看菜单毫不陌生,这份熟悉感正是两框架 API 高度同源的体现。
两者都提供 describe/it/test、beforeEach/beforeAll、expect 加各种 matcher。核心断言写法完全一致:expect(actual).toBe(expected)、toHaveLength、toContain 都通用。唯一明显差别:Vitest 默认不开启全局 API,需在配置里设 globals: true,否则要显式 import { describe, it, expect } from 'vitest'。
参数化测试两者都有:Jest 用 test.each,Vitest 用 it.each,语法几乎相同;跳过用例也都是 xdescribe、xit 或 describe.skip、it.skip。生命周期钩子上,Jest 有 beforeAll/afterEach,Vitest 完全对齐,还额外支持 onTestFailed 这类更精细的钩子,方便在失败瞬间做现场采集。
两家的“点菜单”高度兼容,你在 J 记学会的写法,到 V 家基本无缝上手。这也解释了为什么迁移成本低:改了厨房,没改菜谱。
- describe 定义套件把相关用例归档到一组,等价于给菜单做分类。
- it 定义单条用例一个 it 块就是一道待验证的菜,包含名字和执行函数。
- expect 发起断言把实际值交给匹配器,等待比对。
- matcher 判定结果匹配器返回通过或失败,常见有 toBe、toHaveLength、toContain。
Mock与Spy能力
后厨里最难伺候的是“外部供应商”。测一道菜时不想真的给供应商打电话订食材(避免网络请求、真实支付),就得用“模拟食材”——这就是 Mock。而 Spy 则像在旁边装了个摄像头,盯着某个厨师有没有真的被叫去干活,而不替换他的手艺。
Jest 里用 jest.fn() 创建模拟函数,jest.spyOn(obj, 'method') 观察对象方法;Vitest 对应的是 vi.fn() 和 vi.spyOn()。两者返回值都带 calls 与 mock 属性,记录调用次数、入参和返回值。对断言来说,toHaveBeenCalledTimes 与 toHaveBeenCalledWith 在两框架里写法完全一致。
模块级拦截是另一个核心能力:jest.mock('./api') 可以把整个模块替换成假实现,vi.mock 作用相同。Fake Timers 也双雄必备:Jest 用 jest.useFakeTimers(),Vitest 用 vi.useFakeTimers(),用于测 setTimeout、轮询等时序逻辑。两者在 mock 体系上的对应关系几乎是逐行的。
模拟食材再顺手,也不能每道菜都全用假货。真正要拦住的是“外部供应商”(网络、数据库、第三方 SDK),自己的切菜逻辑该真刀真枪练。V 家与 J 家在这点上思路一致,只是把 jest 前缀换成了 vi。
- mockFn 记录每次调用每调用一次模拟函数,就把本次参数追加进 calls 列表。
- calls 数组长度数组长度即为总调用次数。
- 断言比对把记录次数与期望次数比较,相等则通过。
快照与覆盖率
餐厅要保证招牌菜每次端出来都长一样,最好的办法是第一次做好时拍一张“标准照”存档,以后每次出餐都跟照片比对。这就是快照测试:第一次运行时把组件输出序列化存入 snapshots 目录,之后每次运行都拿新渲染结果与存档比对,不一致就报错。
Jest 与 Vitest 都内置快照测试,语法一致:expect(component).toMatchSnapshot()。差异在细节:Vitest 用 prettier 格式化快照文件,Jest 则内置自己的格式化逻辑;更新快照都是 -u 或 --update 参数,但 Vitest 对渲染差异的“染色”体验更友好,一眼能看出哪里变了。
覆盖率方面,Jest 内置基于 Istanbul 的 coverage 统计,Vitest 默认用 v8 收集器(也可切回 Istanbul)。两者都能输出 lines、branches、functions、statements 四类指标,并支持 thresholds 配置——覆盖率低于门槛,CI 直接失败。
快照像菜品的“标准照”,覆盖率高则像试菜员走遍了菜单的每一道菜。但要警惕:100% 覆盖率不代表菜好吃,只代表每道菜都有人尝过一口;快照也别乱更新,照片本身拍错了,后续比对就全失真。
- 已覆盖行数测试运行过程中真正被执行命中的代码行数。
- 总行数被测文件里所有有效代码行,不含空行与注释。
- 归一化百分比两数相除后放大到百分制,作为质量健康指标。
ESM与Vite集成
V 家餐厅最大的秘密武器,是它的食材供应链直接对接“标准产区”——ESM 原生模块。J 记虽然也能拿到标准食材,但后厨规定所有食材必须过一遍自家预处理线(转成 CommonJS 格式),凭空多了一道工序,出餐自然慢。
技术本质在于运行机制:Jest 基于 Node 的 CJS 体系加 Babel 转换,遇到 import 语法要先由 babel-jest 或 ts-jest 转成 require;对原生 .mjs 文件的支持至今仍是实验性状态,需要 --experimental-vm-modules 标志。Vitest 底层直接复用 Vite 的模块解析与 esbuild 转换,import 语句原生可跑,不需要额外的语法降级。
对 Vite 项目而言,Vitest 可以直接复用 vite.config 里的 resolve.alias 和 plugins(如 Vue 插件、React 插件),测试环境与应用环境共享同一套模块规则;Jest 则需要单独维护 transform、moduleNameMapper,配置漂移是常见痛点。这也是 Vite 系项目里 Vitest 集成“顺滑得像自家厨房”的根本原因。
V 家的快不是玄学——省掉的正是 J 家每次开工前给食材过预处理线的功夫。标准食材(ESM 模块)直接下锅,多出来的时间全花在真正的出菜(执行断言)上。
- Jest 总耗时每个模块都经过 Babel 转换,再执行测试。
- Vitest 总耗时esbuild 预处理极快且按需转换,执行占比更高。
- 加速比公式两式相除,模块数 N 越大,比值越接近两者的转换速度之比。
迁移成本与步骤
如果老板最终决定从 J 记后厨搬进 V 家,最关心的是“搬家费多少、要不要重新装修”。好消息是:两家的菜单几乎一样,绝大多数菜谱(测试用例)可以原封不动,真正要换的是后厨管道(配置文件)和少数工具代号(jest 前缀)。
迁移分三步走:第一步迁移配置文件,把 jest.config.js 里的 testEnvironment、setupFiles 映射到 vite.config.ts 的 test 字段,同时把 npm script 里的 jest 换成 vitest;第二步全局替换 API 前缀,把 jest.fn、jest.mock、jest.spyOn 等替换成 vi 前缀,纯文本替换即可;第三步处理边界差异,比如 Jest 的 moduleNameMapper 对应 Vitest 的 resolve.alias,快照格式变动需要重新生成一次快照。
成本可以从两个维度量化:改动文件的百分比(通常只有 10%-20% 的测试文件需要动手,其余 import 与断言写法不变)与 CI 集成的改动量(GitHub Actions 里把 npx jest 换成 npx vitest run)。如果项目深度依赖 jest 生态插件(如 jest-sonar-reporter),还要找替代品或做额外适配。
搬家总结:厨房(构建体系)换了,菜谱(测试逻辑)大多没动。预算充足、团队熟悉 Vite,这笔搬家费通常一周内回本;如果团队已在 Jest 上沉淀大量插件与习惯,就让 J 记继续好好营业,别为跟风搬家。
- 配置重写项jest.config.js 转成 vite.config.ts 中的 test 块,是权重最高的一步。
- API 替换项jest.xxx 前缀替换为 vi.xxx,用带权重的出现次数表示工作量。
- 边界差异项快照目录重新生成与 jest 专属插件替换带来的额外成本。
AI 应用场景
Vite 驱动的中后台项目测试底座
阶段:工程化基建
解决的问题:传统工具链里测试与构建各维护一套转换规则,容易出现“开发能跑、测试报错”的割裂。Vitest 原生支持 ESM,让动态 import 加载模型配置这类 AI 产品常见场景在测试环境下表现一致,调试体验远优于 Jest。
具体案例:一个 AI 对话产品的管理后台,组件按需加载提示词模板(import() 动态导入)。团队在 Vite 项目里接入 Vitest,300 个测试文件的启动耗时从 Jest 的 4.2 秒降到 1.1 秒,CI 单测阶段整体缩短 3 分钟。
存量 Jest 项目的渐进式迁移
阶段:技术债治理
解决的问题:老项目深度使用 Jest,直接推翻重来风险高。渐进式迁移让新旧测试并行运行,每迁移一个模块立刻获得速度红利,同时用脚本批量替换 jest 前缀,降低人工出错率。
具体案例:某可视化平台把 1200 个测试文件按模块分批迁移,先迁纯函数与工具库(改动最小、收益最大),再处理组件级快照。配合 API 映射脚本,两个月完成 80% 迁移,CI 时间减半。
组件库质量门槛
阶段:质量保障
解决的问题:组件库需要“每道菜都有标准照”——快照测试防止 UI 意外变动;覆盖率阈值把核心组件锁死在合理水位,防止回归悄悄溜进发布流程。
具体案例:AI 应用里的图表组件库(Vue3 + Vite)配 Vitest 与 @vue/test-utils,每个组件配套快照与交互测试;发布前执行 vitest run 并配覆盖率阈值 lines >= 80,低于门槛直接阻断发布流水线。
Python 演示
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""A tiny converter from Jest test files to Vitest test files.""" import sys # Mapping from Jest API to Vitest APIAPI_MAP = { "jest.fn(": "vi.fn(", "jest.mock(": "vi.mock(", "jest.spyOn(": "vi.spyOn(", "jest.useFakeTimers(": "vi.useFakeTimers(", "jest.clearAllMocks(": "vi.clearAllMocks(", "jest.resetModules(": "vi.resetModules(",} def convert_line(line): """把一行 Jest 代码替换成对应的 Vitest 写法""" for jest_api, vitest_api in API_MAP.items(): if jest_api in line: return line.replace(jest_api, vitest_api, 1) if "from '@jest/globals'" in line: return line.replace("@jest/globals", "vitest").replace("jest", "vi") return line def convert_source(source): """逐行转换完整的测试文件源码""" lines = source.splitlines() return "\n".join(convert_line(line) for line in lines) def main(): if len(sys.argv) < 2: print("Usage: python jest_to_vitest.py <test-file>") return 1 file_path = sys.argv[1] try: with open(file_path, "r", encoding="utf-8") as f: source = f.read() except OSError as exc: print("Failed to read file:", exc) return 1 print(convert_source(source)) return 0 if __name__ == "__main__": sys.exit(main())
API_MAP = { "jest.fn(": "vi.fn(", ... }建立“旧 API → 新 API”的映射表,相当于搬家清单。if jest_api in line:检查当前行是否包含待替换的旧 API。return line.replace(jest_api, vitest_api, 1)只替换一次并立即返回,避免同一行被反复处理。if "from '@jest/globals'" in line:处理从 @jest/globals 显式导入 jest 的写法,换成从 vitest 导入 vi。lines = source.splitlines()按行拆分源码,逐行转换。return "\n".join(convert_line(line) for line in lines)转换完成后再用换行符拼回源码。
常见误区
误区我一直以为 Vitest 是 Vite 的附属插件,只有项目用了 Vite 才能上 Vitest,所以老项目碰都不敢碰。
正解Vitest 打包自带了 Vite 引擎,任何 Node 项目都能直接运行,只是普通项目需要额外写 alias、plugin 等适配;而 Vite 项目能省掉这些配置,体验最顺。就像 V 家餐厅自带全套厨具,哪怕你的店不在它家供应链上也能开业,只是接入它家体系更省事。
为何常见:名字太像、官方文档首页全是 Vite 项目演示,很容易让人误以为两者是硬绑定关系。
误区听说 Jest 不支持 ESM,我们的项目全用 import 语法,所以只能选 Vitest。
正解Jest 通过 babel-jest 或 ts-jest 把 ESM 语法转成 CJS 运行,绝大多数场景都工作得很好;只是对原生 .mjs 文件与实验性 ESM 支持仍有限制,需要 --experimental-vm-modules 标志。准确说法是:Jest 能“处理”ESM 语法,但不是“原生运行”ESM,速度和边界体验不如 Vitest。
为何常见:社区里“Jest ESM 支持是实验性”的说法被简化成“不支持”,以讹传讹。
误区一想到迁移就要改几百个测试文件,工作量太大,干脆继续用 Jest 吧。
正解绝大多数测试文件只用 describe/it/expect 这套通用 API,迁移时原样保留;真正要改的是 jest.xxx 前缀(改成 vi.xxx)和配置文件。典型组件库项目改动文件占比通常在 10%-20%,用本文的转换脚本跑一遍就能摸底。
为何常见:把“换框架”类比成“换语言”,高估了 API 差异;实际上两者 API 高度同源,迁移是改管道而非改菜谱。
误区CI 里覆盖率必须 100%,不然就是测试写得不够努力,团队该加班补用例。
正解覆盖率只衡量“哪些代码被跑到”,不衡量“跑得对不对”。为了凑 100% 写一堆不痛不痒的断言,反而降低测试可维护性。更合理的做法:核心业务模块设 80% 门槛,工具函数可放宽到 60%,把精力放在关键路径的行为验证上。
为何常见:覆盖率是最直观的“安全数字”,管理者容易拿它当 KPI,团队也会为了数字好看而堆测试。
面试题
Q1. Vitest 这个测试框架,主要是基于哪个构建工具打造的?
- A.基于 Babel 编译器
- B.基于 Vite 构建器← 正确答案
- C.基于 webpack 打包
- D.基于 Rollup 打包
答案:B。Vitest 由 Vite 团队打造,底层复用 Vite 的模块解析与转换管道,所以选 B。A 的 Babel 是 Jest 转换链路的底层,C 和 D 是其他打包器,都不是 Vitest 的构建基础。
Q2. 在 Vitest 中,原先 jest.fn() 创建模拟函数的写法应改成哪种?
- A.mock.fn() 调用形式
- B.vi.fn() 调用形式← 正确答案
- C.spy.fn() 调用形式
- D.fake.fn() 调用形式
答案:B。Vitest 用 vi 前缀统一管理 mock、spy、fake timer 等测试工具,jest.fn() 对应 vi.fn(),jest.mock() 对应 vi.mock()。就像搬家后工具改了名字,用途完全一样。
Q3. 测试覆盖率公式 Coverage = L_covered / L_total 中的 L_total 表示什么?
- A.全部代码文件的数量
- B.测试文件的总行数
- C.被测代码总行数← 正确答案
- D.通过用例的数量
答案:C。L_total 是被测代码的总有效行数,L_covered 是测试执行命中的行数。把测试当试菜,L_total 是菜单上所有菜,L_covered 是真正被尝过的菜,两者相除才是覆盖率。
Q4. 在 Vitest 中,想观察某个已有对象的方法是否被调用,应该使用哪个 API?
- A.vi.fn() 创建假函数
- B.vi.mock() 拦截整个模块
- C.vi.spyOn() 观察指定方法← 正确答案
- D.vi.useFakeTimers() 伪造计时
答案:C。vi.spyOn() 只“观察”已有对象的方法而不替换实现,适合验证某函数是否被调用,相当于装摄像头。vi.fn() 是造新假函数,vi.mock() 是替换整个模块,vi.useFakeTimers() 是伪造时间。
Q5. 快照测试第一次运行时会发生什么?
- A.直接失败并删除快照
- B.生成快照文件保存基线← 正确答案
- C.跳过当前所有断言
- D.强制要求人工审核通过
答案:B。首次运行没有可比对的基线,框架会生成快照文件并保存,后续运行才拿新输出与基线比对。这相当于第一次给招牌菜拍照存档,之后每次出餐对照照片查差异。
Q6. 关于 Jest 与 Vitest 的对比,下列哪些说法是正确的?
- A.Jest 社区生态更成熟一些← 正确答案
- B.Vitest 原生支持 ESM 模块← 正确答案
- C.Vitest 必须依赖 Babel
- D.Jest 启动速度明显更快
答案:AB。A 正确,Jest 生态的插件、教程和周边库更全;B 正确,Vitest 复用 Vite 的 esbuild 转换,原生支持 ESM。C 错在 Vitest 不依赖 Babel,D 错在 Jest 的启动速度反而不占优势。
Q7. 为什么 Jest 处理 ESM 模块需要额外的配置?
- A.因为 Jest 只能运行于浏览器
- B.因为底层默认走 CJS 转换← 正确答案
- C.因为工具无法识别 import
- D.因为 ESM 规范尚未发布
答案:B。Jest 基于 CJS 生态构建,默认通过 Babel 把 import 转成 require 才能运行,所以需要额外配置。A 错在 Jest 跑在 Node 环境;C 错在 Babel 完全能解析 import 语法;D 错在 ESM 早已成为标准规范。
Q8. 从 Jest 迁移到 Vitest,通常需要完成哪些工作?
- A.把 jest. 前缀替换为 vi.← 正确答案
- B.重写全部断言逻辑
- C.将配置迁移到 vite.config← 正确答案
- D.更新 npm 脚本的执行命令← 正确答案
答案:ACD。迁移需要替换 jest.xxx 前缀为 vi.xxx、把 jest.config.js 的字段映射进 vite.config.ts 的 test 块、把 npx jest 换成 npx vitest run。B 不需要,因为 describe/it/expect 等断言逻辑两框架完全兼容。
Q9. Vitest 默认使用的覆盖率收集器是哪一个?
- A.官方自研的 v8 引擎工具← 正确答案
- B.Babel 自带的转换工具
- C.Webpack 内置的分析器
- D.ESLint 的规则解析器
答案:A。Vitest 默认使用 v8 作为覆盖率收集器(也可配置为 Istanbul),两者都能输出 lines、branches、functions、statements 四类指标。Babel、Webpack、ESLint 都不是覆盖率统计工具。
Q10. Vitest 在非 Vite 项目中也能正常使用,原因包括哪些?
- A.打包自带了默认构建引擎← 正确答案
- B.无配置时会生成一份默认配置← 正确答案
- C.必须手写完整 Vite 配置才能跑
- D.仅能处理纯 JavaScript 文件
答案:AB。Vitest 直接打包了 Vite 引擎,即使项目没有 vite.config,它也会用一份默认配置独立运行,因此非 Vite 项目可用。C 错在不需要完整手写配置,D 错在它同样支持 TypeScript 等文件。