测试金字塔
大量单元测试(快、隔离)、适量集成测试(跨模块真实协作)、少量 E2E(端到端全链路)。比例失衡会导致"改不动"。
测"输入-输出"与关键边界,而非逐行实现。实现改动不应大改测试。
内置 node:test
Node 20 自带 node:test 与 node:assert,无需额外依赖即可跑单元测试。
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('加法正确', () => {
assert.equal(1 + 1, 2);
});
test('async 测试', async () => {
const r = await fetchUser(1);
assert.ok(r.id === 1);
});node --test # 运行 test 目录下的 *.test.jsVitest / Jest
Vitest 兼容 Jest API、原生 ESM、速度更快;Jest 生态成熟。提供 describe/it/expect 结构。
import { describe, it, expect } from 'vitest';
describe('calc', () => {
it('返回两数之和', () => {
expect(add(1, 2)).toBe(3);
});
});断言与匹配
用 matcher 表达期望:toBe/toEqual(值/结构)、toThrow(异常)、resolves/rejects(异步)。
expect(foo).toEqual({ a: 1 });
await expect(p).rejects.toThrow('boom');
expect(() => bad()).toThrow();集成测试 supertest
supertest 直接对 Express/Fastify app 发起 HTTP 断言,无需起真实端口,验证路由+中间件+校验全链路。
import request from 'supertest';
import { app } from '../src/app.js';
it('GET /health 返回 200', async () => {
const res = await request(app).get('/health');
expect(res.status).toBe(200);
expect(res.body.ok).toBe(true);
});Mock 与替身
用 mock 隔离数据库、第三方 API 等慢/不稳定依赖,让单测快且确定。Vitest 提供 vi.fn/vi.mock。
import { vi } from 'vitest';
const sendMail = vi.fn().mockResolvedValue(true);
await service.notify();
expect(sendMail).toHaveBeenCalledOnce();优先 mock 进程外的依赖(网络、DB、时钟),内部纯函数直接测,避免过度 mock 导致测试失真。
覆盖率
覆盖率衡量代码被测试触及的比例,但不能替代质量。目标先达关键路径,再逐步提升。
vitest run --coverage
npx c8 node --test # 内置 runner 配 c8100% 覆盖不等于无 bug(可能只走通没断言行为)。关注"分支覆盖"与"关键断言"。
常见陷阱与 FAQ
用 beforeEach/afterEach 重置状态与 mock,保证用例独立可单独运行。
测试在断言前就结束,失败也可能被吞掉。确保返回 Promise 或显式 await。
可用 Testcontainers 起真实库,或用内存库(如 sqlite :memory:)保证速度与真实性的平衡。