代码写得再漂亮,没有测试的保护,每一次修改都像是在悬崖边行走。单元测试的价值不在于“证明代码没有问题”,而在于当代码出问题时你能第一时间知道,并且能精准定位。对 JavaScript 项目来说,Jest 和 Vitest 是目前最主流的两套测试框架,它们都提供了零配置起步、断言库内置、Mock 功能齐备的完整测试体验。
为什么需要单元测试
在实际项目中有几个非常真实的场景:
- 你修改了一个工具函数,但不知道有没有影响其他调用方。
- 你重构了某个模块的内部实现,想确认外部行为没有变化。
- 你接手了一个没有文档的老项目,想知道某个函数在各种输入下会输出什么。
- 你和同事并行开发,他的改动不小心破坏了你的逻辑。
单元测试就是针对这些问题的最轻量保险——用代码验证代码的正确性。它虽然不能替代集成测试和端到端测试,但写在最底层的单元测试运行速度最快,反馈最及时,是测试金字塔的坚实基座。
Jest:久经考验的业界标准
Jest 由 Facebook 开源,是 React 官方推荐的测试框架,也广泛用于 Vue、Node.js 等各类项目。它的最大特点是 开箱即用——安装后几乎不需要配置就能运行测试。
快速上手
npm install --save-dev jest
在 package.json 中添加脚本:
{
"scripts": {
"test": "jest"
}
}
然后编写一个测试文件 math.test.js:
// 被测函数
function sum(a, b) {
return a + b;
}
// 测试用例
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
运行 npm test,Jest 会自动找到所有 .test.js 或 .spec.js 文件,执行里面的测试用例,并输出清晰的结果报告。
Jest 的核心能力
- 内置断言库:
expect(value).toBe(expected)、.toEqual()、.toContain()、.toThrow()等,语义清晰。 - Mock 功能强大:
jest.fn()创建模拟函数,jest.mock()可以模拟整个模块,非常适合隔离外部依赖。 - 快照测试:
expect(component).toMatchSnapshot()用于 UI 组件的回归测试,检测渲染输出是否意外改变。 - 覆盖率报告:
jest --coverage一键生成语句、分支、函数、行覆盖率报告。 - 并行执行:每个测试文件在独立的进程中并行运行,充分利用多核,提高执行速度。
一个更贴近实战的例子
// userService.js
import axios from 'axios';
export async function getUser(id) {
const response = await axios.get(`/api/users/${id}`);
return response.data;
}
// userService.test.js
import axios from 'axios';
import { getUser } from './userService';
jest.mock('axios');
test('getUser returns user data', async () => {
const mockUser = { id: 1, name: 'Alice' };
axios.get.mockResolvedValue({ data: mockUser });
const result = await getUser(1);
expect(result).toEqual(mockUser);
expect(axios.get).toHaveBeenCalledWith('/api/users/1');
});
通过 jest.mock 将 axios 替换为模拟模块,测试不再依赖真实网络,快速且稳定。
Vitest:为 Vite 生态而生的新星
Vitest 是伴随 Vite 成长起来的测试框架,它充分利用 Vite 的快速编译能力,为现代前端项目提供极速的测试体验。如果你已经在用 Vite 作为构建工具,Vitest 几乎是无缝的选择。
快速上手
npm install --save-dev vitest
如果项目已经使用 Vite,Vitest 会自动复用 vite.config.js 中的配置(如路径别名、插件等),这意味着测试环境与生产构建环境高度一致,避免了“测试能通过但打包报错”的麻烦。
测试文件的语法与 Jest 高度兼容:
import { describe, it, expect } from 'vitest';
describe('sum function', () => {
it('adds two numbers', () => {
expect(sum(1, 2)).toBe(3);
});
});
Vitest 的独特优势
- 原生 ESM 支持:不需要将
import/export转换为 CommonJS,与现代前端项目的模块系统完全一致。 - HMR 驱动的智能监听模式:
vitest --watch可以利用 Vite 的模块热替换,只重新执行受影响的测试,反馈极快。 - 实验性的浏览器模式:可以直接在真实的浏览器环境中运行测试,验证 DOM 操作和 Web API。
- 与 Vite 生态深度集成:可以直接使用 Vite 的插件,也能直接运行
.ts、.tsx文件而无需额外配置。
Jest 与 Vitest 的对比选型
两者不是零和博弈,而是面向不同项目阶段的工具。下表列出关键差异:
| 维度 | Jest | Vitest |
|------|------|--------|
| 构建依赖 | 内置 transformer,需要配置 | 复用 Vite,零配置起步 |
| 启动速度 | 大型项目较慢(依赖解析、编译) | 利用 esbuild 预构建,启动极快 |
| ESM 支持 | 需额外配置(实验性支持) | 原生支持 |
| 生态环境 | 极其成熟,文档和第三方资源丰富 | 增长迅速,社区活跃 |
| 学习成本 | 高(配置多,坑点多) | 低(若已熟悉 Vite) |
选型建议:
- 新项目如果计划使用 Vite,Vitest 几乎是首选。
- 大型遗留项目或 React 项目(特别是使用 Create React App 的)可能已经深度集成了 Jest,迁移有成本,继续使用 Jest 完全没问题。
- 如果你需要在一套测试中同时覆盖 Node.js 和浏览器环境,Vitest 的浏览器模式可能是一个加分项。
- API 和断言语法两者几乎相同,从 Jest 切换到 Vitest 几乎没有学习成本。
构建测试体系的实践原则
框架只是工具,测试的质量取决于写测试的习惯。
- 从工具函数开始:纯函数最容易测试,也是性价比最高的测试切入点。
- 遵循 AAA 模式:Arrange(准备数据)、Act(执行被测函数)、Assert(断言结果),让测试结构清晰。
- 一测试一场景:每个测试只验证一种行为,失败了能立刻定位原因。
- 不追求 100% 覆盖率:覆盖率是参考,不是目标。关键业务逻辑和容易出错的地方要覆盖,简单的 getter/setter 不必逐行测试。
- 持续运行:把测试脚本挂到 Git hooks 或 CI 流水线中,确保每次提交都通过已有的测试用例。
单元测试不是负担,而是你在代码世界里行走的安全绳。投入一点时间搭建好 Jest 或 Vitest 的测试体系,未来的你和你的同事都会感谢现在这个“提前一步”的决定。