测试策略与 Jest
本教程共 76 篇 · 第 61 篇 · 更新于 2026-07-25 · 约 8 分钟阅读
61. 测试策略与 Jest
本节目标:- 测试金字塔:单测、集成、E2E 各自的职责与边界 - TDD(测试驱动开发)的基本节奏 - Jest / Mocha / Vitest / node:test 怎么选 - Jest 实战:安装、
describe/it/expect、mock、覆盖率、watch
上一章我们用了 Node.js 内置的 node:test。它够轻、零依赖,但真到了大项目,你可能会想要更「全家桶」的东西:开箱即用的断言库、更顺手的 mock、快照测试、覆盖率一键生成。这就是 Jest 这类框架存在的理由。
不过,工具只是手段。比「用哪个框架」更重要的是「测什么、怎么分层」。这一章我们先聊测试策略——单元测试、集成测试、端到端测试到底差在哪,再横向对比几个主流框架,最后上手 Jest。
测试金字塔
测试不是越多越好,也不是越细越好。业界有个经典模型叫「测试金字塔」:底层是大量的单元测试,中间是较少的集成测试,顶层是更少的端到端(E2E)测试。越往上越慢、越脆弱、越贵。
单元测试
只测最小的一块逻辑——一个函数、一个方法,把它依赖的东西都 mock 掉,只看自己这块对不对。上一章的 add、divide 测试就是典型单测。它跑得飞快,一个项目几千条单测也能在几秒内跑完。
// 单测:只验证函数本身,数据库/网络都换成替身
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
function isAdult(user) {
return user.age >= 18;
}
describe('isAdult', () => {
it('成年人返回 true', () => {
assert.equal(isAdult({ age: 23 }), true);
});
it('未成年返回 false', () => {
assert.equal(isAdult({ age: 12 }), false);
});
});
集成测试
把几个组件真正拼起来跑,比如「路由 + 控制器 + 真实(或测试用)数据库」。它验证的是「接缝处」,也就是模块之间是不是真能协同工作。单测全绿但集成挂掉,多半是接口对不上。
下面这个例子起一个真实 HTTP 服务,再发请求验证响应:
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import http from 'node:http';
import app from './app.js'; // 导出的 Express 应用
describe('GET /users', () => {
it('返回两个用户', async () => {
const server = http.createServer(app);
await new Promise(resolve => server.listen(0, resolve));
const port = server.address().port;
try {
const res = await fetch(`http://localhost:${port}/users`);
assert.equal(res.status, 200);
const users = await res.json();
assert.equal(users.length, 2);
} finally {
await new Promise(resolve => server.close(resolve));
}
});
});
注意 finally 里一定要把服务关掉,否则测试进程不会退出,会卡住 CI。我踩过这个坑,排查半天以为测试挂了,其实是端口没释放。
端到端测试(E2E)
把整个应用当黑盒,用真实浏览器或真实客户端去操作,连数据库、缓存、第三方服务都是真的。这类测试最贴近用户真实体验,但也最慢、最易碎。常用 Playwright、Cypress 这类工具驱动浏览器点击操作。一般只挑核心流程(登录、下单、支付)来覆盖,不求全。
Note一个经验法则:E2E 里不要 mock。E2E 的价值就在于「全链路真实」,一旦把关键依赖换成假货,测的就不是真实行为了。
TDD:先写测试再写实现
测试驱动开发(Test-Driven Development,TDD)是一种工作流:先写会失败的测试,再写刚好让测试通过的最小实现,最后重构。循环是「红 → 绿 → 重构」。
// 1. 先写测试(此时 validatePassword 还不存在,必然失败)
import assert from 'node:assert/strict';
assert.equal(validatePassword('abc12'), false); // 太短
assert.equal(validatePassword('abcdef123'), true); // 够长且有数字
// 2. 写最小实现让它通过
function validatePassword(pw) {
if (pw.length < 8) return false;
if (!/\d/.test(pw)) return false;
return true;
}
// 3. 重构、加新需求,重复循环
TDD 不是银弹,但它有个实在的好处:逼你想清楚「这个函数到底要被怎么用」才会动手写。对 API 设计和边界条件考虑得更周全。
框架怎么选
市面主流就这几个,各有地盘:
| 框架 | 是否内置 | 零配置 | 内置 mock | 覆盖率 | 适合场景 |
|---|---|---|---|---|---|
| node:test | ✅ 内置 | ✅ | 基础 | 需 V8 工具 | 轻量项目、不想加依赖 |
| Jest | ❌ | ✅ | ✅ 强 | ✅ 内置 | 全家桶、前后端通吃 |
| Mocha | ❌ | ❌ | ❌ 需 Sinon | 需 nyc | 灵活、插件多 |
| Vitest | ❌ | ✅ | ✅ | ✅ 内置 | 已用 Vite、要快、TS 好 |
我的建议:小工具、库、想保持零依赖,直接用 node:test;业务项目、团队协作、要快照和丰富 mock,上 Jest;如果项目已经基于 Vite 构建,Vitest 几乎零成本接入,且速度更快。
Tip别为了「看起来专业」硬上重型框架。我见过一个几百行的小脚本配了全套 Jest + 覆盖率阈值,维护成本比代码本身还高。
Jest 上手
Jest 是 Facebook 出品,主打「开箱即用」。先装:
npm install --save-dev jest
Jest 默认认 *.test.js 和 __tests__/ 目录,基本不用配置就能跑。写法和 node:test 很像,但断言换成了 expect(...).toBe(...) 这种「期望式」语法:
// utils/math.js
export function sum(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Both arguments must be numbers');
}
return a + b;
}
export function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// __tests__/math.test.js
import { sum, divide } from '../utils/math.js';
describe('Math 工具', () => {
describe('sum()', () => {
it('正确相加', () => {
expect(sum(1, 2)).toBe(3);
expect(sum(-1, 1)).toBe(0);
});
it('非数字抛错', () => {
expect(() => sum('1', 2)).toThrow('Both arguments must be numbers');
});
});
describe('divide()', () => {
it('正确相除', () => {
expect(divide(10, 2)).toBe(5);
});
it('除零抛错', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
expect 的 matcher 很丰富:toBe 严格相等、toEqual 深比较、toThrow 测异常、toMatch 正则匹配等等。报错信息也比 assert 友好,会直接把期望值和实际值并排打出来。
运行与常用开关
# 跑全部
npx jest
# 监听模式,改文件自动重跑
npx jest --watch
# 只跑名字匹配的用例
npx jest -t "sum"
# 生成覆盖率报告(开箱即用)
npx jest --coverage
Jest 的 --coverage 不用装任何额外工具,直接给你行级、分支、函数、语句四个维度的覆盖率,还会在终端画出表格。这是它相对 node:test 最省心的地方。
Jest 的 mock
Jest 自带一套完整的 mock 机制,比 node:test 的更顺手。
// 造一个函数替身
const mockFn = jest.fn();
mockFn('hello');
expect(mockFn).toHaveBeenCalledWith('hello');
// mock 一个模块
jest.mock('axios');
import axios from 'axios';
axios.get.mockResolvedValue({ data: { ok: true } });
jest.fn() 记录调用,jest.mock() 把整个模块换掉。对于「不想真发网络请求」的场景,这套组合拳用起来比手动写替身舒服。
快照测试
Jest 带火了快照测试(Snapshot):第一次运行时把输出存成快照文件,之后每次运行都和快照比对。UI 渲染、配置文件输出这类「结构稳定但手写断言麻烦」的东西特别适合:
it('渲染结构稳定', () => {
const tree = renderUserProfile(user);
expect(tree).toMatchSnapshot();
});
Warning快照不是免死金牌。它容易「脆性」——你只是改了个无关紧要的文案,快照就报错,于是有人习惯性地
jest -u一键更新,等于变相关掉了测试。快照要当作契约来对待,更新前先想清楚为什么变了。
把策略落到项目里
一个务实的做法:
- 单测占大头,覆盖纯逻辑、工具函数、边界与异常分支,跑得快,提交前本地就跑。
- 集成挑关键路径,比如「注册接口真的写进了数据库」。
- E2E 只覆盖最值钱的用户旅程,放 CI 里夜里慢慢跑。
框架层面,新项目从 node:test 起步完全够用,等你觉得想要快照、想要更丝滑的 mock、想要零配置覆盖率时,再迁移到 Jest 也不迟——两者的用例写法高度相似,迁移成本主要在断言语法上。