常用 JavaScript/TypeScript 测试框架的横向对比与选型建议。快速参考——实践示例见各课程。
| 框架 | 代际 | 发布时间 | 理念 | 一句话 |
|---|---|---|---|---|
| Mocha + Chai + Sinon | 第一代 | 2012 | 拼接式:自己组装 runner、断言库、mock 库 | 极其灵活,但配置成本高,新项目不推荐 |
| Jest | 第二代 | 2015 | 一体化:零配置、内置断言/mock/coverage/快照 | 生态最大,但 ESM 支持弱、TypeScript 配置痛 |
node:test | 第二代 | 2022 (Node 18) | 内置:Node.js 原生,零依赖 | 适合纯函数 + fixture 场景,mock 能力有限 |
| Vitest | 第三代 | 2022 | Vite 驱动:原生 ESM、极速 HMR、Jest 兼容 API | 2026 年新项目最推荐 |
| 维度 | Mocha | Jest | node:test | Vitest |
|---|---|---|---|---|
| 零配置开箱即用 | ❌ 需装 3-4 个包 | ✅(但有坑) | ✅ 真正零依赖 | ✅ |
| TypeScript 支持 | 需 ts-node/tsx | 需 ts-jest 或 @swc/jest | 需 tsx 或先 tsc | ✅ 原生理解 TS |
| ESM 支持 | ⚠️ 需配置 | ⚠️ experimental | ✅(Node 原生) | ✅ ESM-first |
| 内置断言 | ❌ 需 Chai | ✅ expect() | ✅ assert | ✅ expect() |
| 内置 mock/spy | ❌ 需 Sinon | ✅ jest.fn() | ⚠️ Node 22+ 有基础 mock | ✅ vi.fn() |
| 快照测试 | ❌ | ✅ toMatchSnapshot() | ❌ | ✅ toMatchSnapshot() |
| 内置 coverage | ❌ 需 nyc/c8 | ✅ --coverage | ⚠️ --experimental-test-coverage | ✅ --coverage |
| Watch 模式速度 | 慢 | 中等 | 快(无 transform) | 极快(Vite HMR) |
| 并行执行 | ❌ 默认串行 | ✅ 文件级并行 | ✅ 文件级并行 | ✅ 文件级并行 + 线程池 |
| 学习曲线 | 中(需学 3 个库) | 低 | 极低 | 低(Jest 兼容 API) |
| 场景 | 推荐 | 理由 |
|---|---|---|
| 零依赖原型 / 小型 CLI 工具 | node:test | 不需要安装任何东西,Node 18+ 直接可用 |
| 纯函数测试 + fixture 文件 | node:test | 不需要 mock,assert 完全够用 |
| 需要 mock 外部依赖 | Vitest | vi.mock() 比手写 stub/sinon 简洁得多 |
| VS Code 扩展(需 mock vscode API) | Vitest | mock StatusBarItem、window、workspace 等 VS Code API |
| Vite 项目(前端) | Vitest | 复用 vite.config.ts,HMR 级速度 |
| React Native | Jest | RN 工具链深度绑定 Jest |
| 已有 5000+ Jest 测试的存量项目 | Jest | 迁移成本高于收益,除非团队有明确计划 |
| 需要非标准 runner 行为 | Mocha | 程序化 API 最灵活(95% 项目不需要) |
| 2026 年启动的新 JS/TS 项目 | Vitest | 当前最优默认选择 |
import { describe, it, before, after, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; describe('suite', () => { it('case', () => { assert.equal(actual, expected); assert.deepEqual(obj, other); assert.match(str, /regex/); assert.doesNotMatch(str, /bad/); assert.ok(truthy); assert.throws(() => fn()); }); }); // 运行:node --test out/test.js // TypeScript:node --import tsx --test src/test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'; describe('suite', () => { it('case', () => { expect(actual).toBe(expected); expect(obj).toEqual(other); expect(str).toMatch(/regex/); expect(truthy).toBeTruthy(); expect(() => fn()).toThrow(); }); }); // Mock 示例 const mockFn = vi.fn().mockReturnValue(42); vi.mock('fs', () => ({ readFileSync: vi.fn() })); // 运行:npx vitest
// API 与 Vitest 几乎相同——替换 import 来源即可: import { describe, it, expect, jest, beforeEach } from '@jest/globals'; // 或者不 import,依赖 Jest 的全局注入 const mockFn = jest.fn().mockReturnValue(42); jest.mock('fs', () => ({ readFileSync: jest.fn() })); // 运行:npx jest
大致量级对比(200 个测试的套件,纯函数,无 I/O)——精确数字取决于具体项目,但相对关系稳定。
| 框架 | 冷启动 | Watch 增量 | 说明 |
|---|---|---|---|
| Vitest | ~1.2s | ~50ms | Vite HMR——只重跑受影响的测试文件 |
node:test | ~1.5s | ~1.5s | 无 transform 开销,但 watch 模式重跑整个文件 |
| Jest | ~8s | ~2s | ts-jest 做完整类型检查是主要瓶颈 |
| Jest + @swc/jest | ~3s | ~1s | 用 SWC 替代 tsc 做 transform,快不少 |
| Mocha + tsx | ~2s | — | 无内置 watch,需配合 --watch 或 nodemon |
VS Code 扩展测试有两个独特挑战:
// 扩展代码中大量使用: import * as vscode from 'vscode'; vscode.window.createStatusBarItem(...) vscode.workspace.getConfiguration(...) // 在测试中直接 import 'vscode' 会失败—— // vscode 模块只在 VS Code 扩展宿主进程中存在。 // 解决方案:mock 掉整个 vscode 模块。
Vitest 的 vi.mock() 最适合这个场景:
// __mocks__/vscode.ts export const window = { createStatusBarItem: vi.fn(() => ({ show: vi.fn(), text: '' })), showInformationMessage: vi.fn(), }; export const workspace = { getConfiguration: vi.fn(() => ({ get: vi.fn() })), };
如果只用 node:test,你需要手写 stub 或引入 sinon——都比 vi.mock() 更啰嗦。
纯数据变换函数(getLatestTokenCount、decodeProjectPath、formatTokens)不碰 VS Code API——用 node:test + fixture 文件测试完全足够。这就是 Lesson 0007 选 node:test 的原因:在不需要 mock 的时候,零依赖是最优解。
| 从 | 到 | 难度 | 说明 |
|---|---|---|---|
node:test | Vitest | 低 | API 不同但逻辑相同——改写断言语法即可。当 mock 需求出现时迁移 |
| Jest | Vitest | 低 | API 几乎一样——改 import 来源、删掉 jest.config.ts、装 vitest 即可 |
| Mocha | Vitest | 中 | 需要从 Chai 断言迁移到 expect(),从 Sinon mock 迁移到 vi.fn() |