Angular 测试框架迁移指南:从 Karma 与 Jasmine 平滑切换到 Vitest
本文以 Angular 官方文档(migrating-to-vitest.md)为骨架,结合本仓库中 Angular 核心代码(尤其是 zone.js 提供的 Vitest 补丁实现),系统讲解如何把既有 Angular 项目的单元测试从 Karma + Jasmine 迁移到 Vitest:涵盖手工迁移的五个步骤、可选的浏览器(真实浏览器)运行模式、由 Angular CLI 提供的 refactor-jasmine-vitest 自动化重构 schematic、自定义 Vitest 配置,以及 zone.js Vitest patch 的作用与原理。读完本文,你将能够独立完成一次完整的迁移并规避其中的常见坑点。
重要前提:将既有项目迁移到 Vitest 目前仍被视为实验性能力;此外该流程要求项目使用
application构建体系(applicationbuild system),这也是所有新创建项目的默认配置。新项目默认使用 Vitest 作为单元测试运行器,而存量项目则仍默认使用 Karma。
一、迁移前必读:Karma → Vitest 的整体思路
Angular CLI 已经将 Vitest 作为新项目的默认单元测试运行器。对于老项目而言,迁移的本质分为两条并行主线:
- 运行器层迁移:把
angular.json中testtarget 的 builder 从@angular/build:karma换成@angular/build:unit-test,让测试走 Vitest; - 测试代码层迁移:把测试文件(
.spec.ts)里基于 Jasmine 全局 API 的写法(spyOn、jasmine.objectContaining、fit/fdescribe等)改写为 Vitest 等价写法。
第一条主线依赖 Angular 应用构建体系(application builder)——只有新构建体系才能被 unit-test builder 复用其编译产物;第二条主线可以手工完成,也可以交给官方实验性 schematic refactor-jasmine-vitest 自动完成。
从本仓库的文档结构可以看到 Vitest 相关的完整测试知识地图:guide/testing/overview.md 综述各类测试场景、components-scenarios.md 提供组件测试场景与基于 Vitest fake timers 的异步测试示例、code-coverage.md 说明覆盖率能力,而本文对应的 karma.md 则是尚未迁移用户仍在使用的 Karma 指南。
二、手工迁移步骤(Manual migration steps)
在运行自动化重构 schematic 之前,必须先把项目手工切换到 Vitest 测试运行器。整个过程分为 5 个步骤。
第 1 步:安装依赖
安装 vitest 以及一个 DOM 模拟库。虽然仍可在真实浏览器中测试(见第 5 步),但 Vitest 默认会在 Node.js 中通过 DOM 模拟库来模拟浏览器环境,从而获得更快的执行速度。Angular CLI 会自动探测环境:如果安装了 happy-dom 则优先使用,否则回退到 jsdom——因此这两个包至少必须安装其一。
# npm
npm install --save-dev vitest jsdom
# yarn
yarn add --dev vitest jsdom
# pnpm
pnpm add -D vitest jsdom
# bun
bun add --dev vitest jsdom
提示:想追求更轻量、更快的 DOM 模拟,可安装
happy-dom替代jsdom;安装happy-dom后 CLI 会自动优先选择它。
第 2 步:更新 angular.json
找到项目对应的 test target,把 builder 改为 @angular/build:unit-test:
{
"projects": {
"your-project-name": {
"architect": {
"test": {
"builder": "@angular/build:unit-test"
}
}
}
}
}
unit-test builder 有以下默认值,若你的项目结构不同则需要显式覆盖:
tsConfig:默认tsconfig.spec.json;buildTarget:默认::development(即“项目名:构建目标:development 配置”的简写)。
当项目缺少 development 构建配置、或需要与默认不同的测试选项时,你可以新建一个名为 testing(或其它名称)的构建配置,并把 buildTarget 指向它,例如:
{
"projects": {
"your-project-name": {
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {
"buildTarget": "your-project-name:build:testing"
}
}
}
}
}
}
需要特别注意 builder 能力的差异:旧的 @angular/build:karma 允许把构建类选项(如 polyfills、assets、styles)直接配置在 test target 内部;而新的 @angular/build:unit-test 不再支持这种用法。如果你的测试专用构建选项与既有 development 构建配置不同,就必须把它们迁移到独立的构建 target 配置中;若本来就与 development 配置一致,则无需任何额外动作。
第 3 步:处理自定义 karma.conf.js 配置
karma.conf.js 中的自定义配置不会被自动迁移。在删除该文件前,务必逐项审查其中的自定义设置。
大多数 Karma 选项在 Vitest 中都有对应物,可以写入自定义 Vitest 配置文件(如 vitest.config.ts),再通过 angular.json 的 runnerConfig 选项挂接给 builder。常见迁移路径如下:
| Karma 概念 | Vitest 对应方案 |
|---|---|
| Reporters(报告器) | 替换为 Vitest 兼容的报告器,通常可直接在 angular.json 的 test.options.reporters 中配置;更高级的配置使用自定义 vitest.config.ts |
| Plugins(插件) | Karma 插件需要你自行查找并安装对应的 Vitest 等价插件;注意代码覆盖率在 Angular CLI 中是内建一等能力,直接运行 ng test --coverage 即可启用 |
| Custom Browser Launchers(自定义浏览器启动器) | 由 angular.json 中的 browsers 选项 + 安装浏览器 provider(如 @vitest/browser-playwright)取代 |
其余设置请查阅官方 Vitest 配置文档。
第 4 步:删除 Karma 与 test.ts 文件
现在可以从项目中删除 karma.conf.js 和 src/test.ts,并卸载 Karma 相关包。下面命令以全新 Angular CLI 项目默认安装的包为准,你的项目可能还有其它需要清理的 Karma 相关包:
# npm
npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter jasmine-core
# yarn
yarn remove karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter jasmine-core
# pnpm
pnpm remove karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter jasmine-core
# bun
bun remove karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter jasmine-core
第 5 步(可选):配置浏览器模式(Browser mode)
若确实需要在真实浏览器中运行测试(而不使用 Node.js 内置的 DOM 模拟),需要安装浏览器 provider 并配置 angular.json。
安装浏览器 provider,三者选一:
- Playwright:
@vitest/browser-playwright,支持 Chromium、Firefox、WebKit; - WebdriverIO:
@vitest/browser-webdriverio,支持 Chrome、Firefox、Safari、Edge; - Preview:
@vitest/browser-preview,面向 WebContainer 环境(如 StackBlitz)。
# 以 Playwright 为例
npm install --save-dev @vitest/browser-playwright
# yarn
yarn add --dev @vitest/browser-playwright
# pnpm
pnpm add -D @vitest/browser-playwright
# bun
bun add --dev @vitest/browser-playwright
更新 angular.json 启用浏览器模式:在 test target 的 options 中加入 browsers 数组。浏览器名称取决于所装的 provider(例如 Playwright 用 chromium,WebdriverIO 用 chrome):
{
"projects": {
"your-project-name": {
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {
"browsers": ["chromium"]
}
}
}
}
}
}
关于有头/无头模式的自动判定:只要设置了 CI 环境变量,或者浏览器名包含 “Headless”(如 ChromeHeadless),就会自动启用无头(headless)模式;否则测试会在有头(headed)浏览器中运行。
三、用 schematic 自动重构测试代码
重要提示:
refactor-jasmine-vitestschematic 同样处于实验性阶段,不可能覆盖所有测试模式。schematic 产生的所有改动都需要人工复核。
Angular CLI 提供了 refactor-jasmine-vitest schematic,用于把 Jasmine 测试自动重构为 Vitest 写法。
3.1 它能做什么
该 schematic 会对测试文件(.spec.ts)自动执行以下变换:
fit/fdescribe→it.only/describe.only;xit/xdescribe→it.skip/describe.skip;spyOn→ 等价的vi.spyOn;jasmine.objectContaining→expect.objectContaining;jasmine.any→expect.any;jasmine.createSpy→vi.fn;beforeAll、beforeEach、afterAll、afterEach→ 各自对应的 Vitest 钩子;fail()→ Vitest 的vi.fail();- 调整断言(expectations)以匹配 Vitest API;
- 对无法自动转换的代码添加 TODO 注释。
3.2 它不会做什么
明确哪些事情 schematic 不会代办,有助于判断仍需手工处理的清单:
- 不会安装
vitest或其它相关依赖; - 不会修改
angular.json去使用 Vitest builder,也不会把polyfills、styles等构建选项从testtarget 迁移出去(这需要你在第 2 步手工完成); - 不会删除
karma.conf.js或test.ts文件; - 不会处理复杂的、嵌套的 spy 场景——这类情况可能需要手工重构。
3.3 如何运行
待项目完成 Vitest 运行器配置(即前文手工迁移步骤)后,即可重构测试文件。若要重构默认项目中的所有测试文件:
ng g @schematics/angular:refactor-jasmine-vitest
3.4 可用选项
| 选项 | 说明 |
|---|---|
--project <name> |
在多项目工作区中指定要重构的项目。示例:--project=my-lib |
--include <path> |
只重构指定文件或目录。示例:--include=src/app/app.component.spec.ts |
--file-suffix <suffix> |
指定不同的测试文件后缀。示例:--file-suffix=.test.ts |
--add-imports |
当你在 Vitest 配置中关闭了 globals 时,为测试文件显式添加 vitest 导入 |
--verbose |
查看所应用的全部转换的详细日志 |
--browser-mode |
若你打算在浏览器模式下运行测试,则使用该选项 |
3.5 迁移完成后的收尾动作
schematic 结束后,建议按以下顺序确认迁移质量:
- 运行测试:执行
ng test,确认所有测试在重构后依然通过; - 审查改动:仔细检查 schematic 所做的改动,尤其要关注包含复杂 spy / mock 的测试——它们很可能需要进一步的人工调整。
值得说明的是 ng test 的行为差异:该命令会以 watch 模式构建应用并启动已配置的 runner。当处于交互式终端且非 CI 环境时,watch 模式默认开启。
四、Vitest 的配置机制
Angular CLI 会替你承担大部分 Vitest 配置工作:它会根据 angular.json 中的选项,在内存中构建出完整的 Vitest 配置,并不要求项目根目录出现 vitest.config.ts。
4.1 自定义 Vitest 配置(Custom Vitest configuration)
重要提示:使用自定义配置虽然能解锁高级选项,但 Angular 团队不提供对配置文件具体内容的直接支持,也不对其中引用的任何第三方插件负责。为保证正常运行,CLI 还会覆盖若干属性(
test.projects、test.include)。
可通过两种方式提供自定义 Vitest 配置文件,覆盖默认设置(完整选项列表见官方 Vitest 配置文档)。
方式一:直接指定路径
{
"projects": {
"your-project-name": {
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {"runnerConfig": "vitest.config.ts"}
}
}
}
}
}
方式二:自动搜索共享基础配置
把 runnerConfig 设为 true,builder 会自动在项目根目录与工作区根目录搜索共享的 vitest-base.config.* 文件。
五、zone.js 的 Vitest patch:让 fakeAsync 家族继续可用
如果你的既有测试仍在使用 fakeAsync、flush、waitForAsync 这类基于 zone.js 的测试工具,迁移到 Vitest 后它们并不会自动工作——因为 Vitest runner 默认不会为测试过程建立 Zone 上下文。解决办法是在 angular.json 中把 zone.js/plugins/vitest-patch 加入 test target 的 polyfills:
{
"projects": {
"your-project-name": {
"architect": {
"test": {
"options": {
"polyfills": ["zone.js/plugins/vitest-patch"]
}
}
}
}
}
}
5.1 patch 的源码实现与原理
这份补丁并非 CLI 黑盒,其实现就存在于本仓库的 zone.js 中,入口为 packages/zone.js/lib/vitest/rollup-vitest.ts(仅做 patchVitest(Zone) 调用),核心逻辑在 packages/zone.js/lib/vitest/vitest.ts:
- 补丁通过
Zone.__load_patch('vitest', ...)注册,会先检测 Vitest runner 是否注入了全局vitest对象,并用__zone_patch__标记防止重复打补丁(vitest.ts); - 打补丁前会强制校验
ProxyZoneSpec与SyncTestZoneSpec已就位,并据此 fork 出两条 Zone:SyncTestZoneSpec('vitest.describe')用于把describe/suite的 body 放入仅同步 Zone 执行,ProxyZoneSpec用于让it/test与各类钩子在测试执行期间获得正确的异步代理上下文(vitest.ts); - 补丁覆盖了
suite/describe与it/test两组 API 的全部修饰形式:直接修饰符skip、only、concurrent、sequential、shuffle、todo,以及柯里化修饰符skipIf、runIf、each、for(vitest.ts),确保it.only、describe.skip、it.each这类写法同样被正确包裹;同时beforeEach、afterEach、beforeAll、afterAll也会用 ProxyZone 包裹执行(vitest.ts); - 有一个容易被忽视的细节:补丁在包装测试函数时特意同步了函数的
length属性,以便 Vitest 核心正确判断测试函数是否声明了done参数(vitest.ts)。
在打包侧,vitest-patch 作为 zone.js 的标准 bundle 目标之一被声明于 packages/zone.js/bundles.bzl,其入口正是 vitest/rollup-vitest;对应的回归测试见 packages/zone.js/test/vitest/vitest-patch-globals.spec.js。
5.2 关于未来的测试写法
无论如何,官方文档都强烈建议你尽早规划把既有测试套件迁移到原生 async/await 以及 Vitest 内建的 fake timers(mock clock)——这才是长期被推荐的既定路线。
在测试编写层面,你可以参考 guide/testing/components-scenarios.md 中 “Async test with a Vitest fake timers” 一节的现成范例:它展示了在 TestBed 环境下用 vi.useFakeTimers() 启动假定时器、vi.runAllTimersAsync() 推进异步任务、最后 vi.useRealTimers() 恢复真实时钟的完整模式。该节还明确指出:fakeAsync 这类基于 zone.js 打补丁的 mock clock 已不再推荐使用,优先选择原生 async 策略或 Vitest/Jasmine 的 fake timers。
六、迁移后的验证与问题反馈
迁移完成后,除了执行 ng test 验证全部用例通过外,还建议:
- 关注任何依赖精确计时或真实 DOM 事件的用例——Node.js DOM 模拟环境(
jsdom/happy-dom)与真实浏览器的行为存在差异; - 留意被 schematic 标注 TODO 的片段,逐一人工补齐。
如果遇到 bug 或有功能诉求,可以前往 Angular CLI 仓库提交 issue(详见官方文档 migrating-to-vitest.md 末尾的反馈指引),提交时尽量附带可最小复现的样例,以便团队更快定位问题。
七、速查:迁移清单总览
| # | 事项 | 操作要点 |
|---|---|---|
| 1 | 安装依赖 | vitest + jsdom(或 happy-dom),后者可选 |
| 2 | 换 builder | test.builder → @angular/build:unit-test;构建类选项移入独立 build target |
| 3 | 迁移 Karma 配置 | 检查 karma.conf.js 的 reporters / plugins / launchers,迁移到 Vitest 或 angular.json |
| 4 | 清理旧文件与包 | 删除 karma.conf.js、src/test.ts,卸载 karma / jasmine 系列依赖 |
| 5(可选) | 浏览器模式 | 安装 @vitest/browser-* provider 并配置 options.browsers |
| 6 | 重构测试代码 | ng g @schematics/angular:refactor-jasmine-vitest(可选配 project/include/verbose 等) |
| 7 | 配置与 polyfill | 必要时用 runnerConfig 挂自定义 Vitest 配置;用到 fakeAsync 系工具则加 zone.js/plugins/vitest-patch |
| 8 | 验证 | ng test + 逐项审查 schematic 改动,重点检查复杂 spy/mock |
整个迁移链路中,本仓库可为你提供两类一手资料:一是权威的官方指南文档(本文所依据的 migrating-to-vitest.md 及其兄弟文档);二是 zone.js 中可直接阅读的 Vitest 补丁源码与测试,帮助你理解迁移后在 Zone 环境下异步测试的真实行为边界。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00