CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
测试 node:test 内置测试运行器、断言、HTTP 接口测试入门。
测试 node:test
这一课你将能做到
用 Node 20 内置的 node:test 写出单元测试并运行,全程零第三方依赖
使用 assert 断言普通值、抛错(assert.throws)和异步拒绝(assert.rejects)
用 describe/it 组织用例,测异步函数,并跑出覆盖率报告
为什么要学
没有测试的代码,改一行就要「凭感觉」上线——感觉是不可靠的。测试人话:把「我改完应该还能跑」变成「机器跑一遍用例确认还能跑」,重构、升级依赖、接手别人代码时全靠它兜底。
Node 20 把测试框架 node:test 升为稳定内置模块:不用装 Jest/Mocha,node --test 一条命令就能跑。小工具、脚本、库项目用它完全够;理解了它的概念(用例、断言、夹具、覆盖率),换 Jest 等框架也只是换语法外壳。
先建立图像
一个测试文件的结构(AAA 模式):
test('用例描述', () => {
// Arrange 准备:造输入数据
// Act 执行:调用被测函数
// Assert 断言:assert.strictEqual(实际, 期望)
});
运行链路:
node --test → 扫描 test/ 目录(或 *.test.js)→ 逐个执行 → 汇总报告
✓ 用例通过(绿) ✗ 断言失败(红,显示实际 vs 期望)
测试的本质就一句话:用代码描述「给定什么输入、期望什么输出」,让机器反复验证。
跟着做
步骤 1:写一个被测模块
新建 node-test 目录,npm init -y,新建 math.js:
// math.js
function add(a, b) {
return a + b;
}
function divide(a, b) {
if (b === 0) {
throw new RangeError('除数不能为 0');
}
return a / b;
}
async function sumRemote(values) {
// 模拟一个异步计算(现实中可能是查库/请求)
await new Promise((r) => setTimeout(r, 10));
return values.reduce((s, n) => s + n, 0);
}
module.exports = { add, divide, sumRemote };
步骤 2:第一个测试文件
按约定新建 test/math.test.js(或与源文件同级叫 math.test.js,node --test 都认):
// test/math.test.js
const test = require('node:test');
const assert = require('node:assert');
const { add, divide } = require('../math');
test('add 两数相加', () => {
assert.strictEqual(add(1, 2), 3);
});
test('add 处理负数', () => {
assert.strictEqual(add(-1, -2), -3);
});
test('divide 正常相除', () => {
assert.strictEqual(divide(10, 4), 2.5);
});
test('divide 除以 0 时抛 RangeError', () => {
assert.throws(() => divide(1, 0), {
name: 'RangeError',
message: '除数不能为 0',
});
});
运行:
node --test
# ▶ add 两数相加
# ✔ 0ms
# ▶ divide 除以 0 时抛 RangeError
# ✔ 1ms
# ℹ tests 4
# ℹ pass 4
# ℹ fail 0
零配置、零安装,直接出报告。把 npm test 接上(package.json scripts 默认的 test 值改掉):
"scripts": {
"test": "node --test"
}
步骤 3:故意让测试失败——看失败长什么样 test('故意失败示范', () => {
assert.strictEqual(add(1, 2), 99); // 期望值故意写错
});
node --test
# ✖ failing tests:
# ✖ 故意失败示范
# Error [ERR_ASSERTION]: Expected inputs to be identical.
# + actual: 3
# - expected: 99
报告明确告诉你「实际 3、期望 99」。永远先看测试失败一次 (确认测试真的在检查),再改对——没红过的测试不可信。
步骤 4:describe/it 组织用例 + 测异步 用例多了要分组。describe 建分组,it 是 test 的别名(读起来像句子):
// test/math-group.test.js
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { add, divide, sumRemote } = require('../math');
describe('math.add', () => {
it('正数相加', () => {
assert.strictEqual(add(2, 3), 5);
});
it('浮点相加', () => {
assert.strictEqual(add(0.1, 0.2), 0.30000000000000004); // 二进制浮点事实
});
});
describe('math.sumRemote(异步)', () => {
it('async 函数直接 await', async () => {
const result = await sumRemote([1, 2, 3]);
assert.strictEqual(result, 6);
});
it('拒绝的场景用 assert.rejects', async () => {
await assert.rejects(
() => Promise.reject(new Error('网络错误')),
{ message: '网络错误' }
);
});
});
异步用例两条规则:测试函数写成 async,内部 await 被测函数;测「应该拒绝」用 assert.rejects(对应同步的 assert.throws)。忘了 await 是异步测试的头号 bug ——断言在 Promise 落定前就执行了,测试假绿。
步骤 5:测抛错与边界值的完整姿势 // test/boundary.test.js
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { divide } = require('../math');
describe('divide 边界', () => {
it('NaN 直接抛 RangeError(不返回 NaN 更好排查)', () => {
assert.throws(() => divide('a', 2), RangeError);
});
it('结果可用 Math.abs 验证符号', () => {
assert.ok(Math.sign(divide(-10, 2)) === -1);
});
});
assert.throws(fn, RangeError) 可只按类型匹配;传对象则要求字段全部吻合。常用断言速查:
assert.strictEqual(a, b); // 严格相等(===,用于原始值)
assert.deepEqual(obj1, obj2); // 深比较对象结构
assert.ok(value); // 真值断言
assert.match(str, /正则/); // 字符串匹配
assert.throws(fn, ErrorType); // 同步抛错
await assert.rejects(fn, ErrorType); // 异步拒绝
步骤 6:覆盖率——哪些代码没被测到 node --test --experimental-test-coverage
# ℹ start of coverage report
# ℹ -------------------------------------------------------------
# ℹ file | line % | branch % | funcs % | uncovered lines
# ℹ -------------------------------------------------------------
# ℹ math.js | 100.00 | 100.00 | 100.00 |
# ℹ test/math... | 100.00 | | |
# ℹ -------------------------------------------------------------
# ℹ all files | 100.00 | 100.00 | 100.00 |
报告列出每个文件的行/分支/函数覆盖率和未覆盖行号。覆盖率是「找漏」工具不是 KPI:80% 有意义的断言,好过 100% 的 assert.ok(true)。接线到 scripts:
"scripts": {
"test": "node --test",
"test:cov": "node --test --experimental-test-coverage"
}
讲清楚要点
node:test 是 Node 20 内置的稳定测试运行器 :node --test 自动发现 test/ 目录和 *.test.js 文件;断言用同样内置的 node:assert,一条第三方依赖都不用装。
AAA 是用例的组织纪律 :Arrange 准备数据、Act 执行被测函数、Assert 断言结果。一个用例只验证一个行为,描述写成「能当文档读」的句子。
每个用例必须独立 :不依赖执行顺序、不共享可变状态。测试文件之间并行执行,共享可变状态会导致「单独跑能过、一起跑就炸」。
同步用 assert.throws,异步用 assert.rejects :Promise 拒绝不会触发同步 try/catch,测异步错误必须用 rejects + await。
失败优先原则 :新写测试先让它们红一次,确认断言真的在工作;只见过绿的测试可能从第一行起就是假测试。
你可能会懵的地方
不用 Jest 会不会太寒酸?
不会。node:test 已内置且稳定,小到中型的库、脚本、CLI 用它最轻快。Jest/Vitest 的优势在快照、mock 生态和组件测试——等需求出现再引入,概念完全可迁移(describe/it/断言都是同套词汇)。
测试文件放哪、怎么命名?
默认两处都被扫描:项目里任何 *.test.js、或 test/ 目录下的文件。团队约定一种:小项目 test/ 目录 + xxx.test.js 与被测文件同名,一眼对应。
怎么测「会抛错」的函数?
同步:assert.throws(() => fn(), RangeError)。注意传函数引用 而不是调用结果——写成 assert.throws(fn(), ...) 是在断言函数返回值,抛错会直接炸掉测试而不是被捕获。
测试里的 setTimeout 真的要等吗?
真的会等,所以别在用例里写长 sleep。被测函数里的延时用可注入的方式设计(把 delay 或「时钟」作为参数传入),测试传 0 或假实现——这就是「可测试性设计」的入门。
覆盖率 100% 就安全了吗?
不。覆盖率只说明「这行代码执行过」,不说明「断言了正确行为」。assert.ok(true) 刷满行覆盖毫无意义。看覆盖率的正确姿势是找「完全没被碰过」的模块。
错法对照 错法 问题 正解 assert.equal(0, '') 借宽松比较== 语义,0 == '' 为 true,假绿用 assert.strictEqual / deepStrictEqual 测试函数里忘 await 异步调用 断言先于结果执行,测试假绿 测试函数标 async,逐个 await assert.throws(fn()) 传调用结果抛错直接炸测试而非被捕获 传箭头函数 () => fn() 用例间共享可变的全局变量 一起跑时互相污染 每个用例自建数据,或用 beforeEach 重置 一个用例里塞五六个不相关断言 失败时无法定位是哪个行为坏了 一用例一行为,描述即文档 断言失败前先打 console.log 看看 手工目测,不可重复 直接断言,报告会打印 actual/expected
卡点急救
node --test 说找不到测试 :确认文件名匹配 *.test.js 或位于 test/ 目录;在项目根目录运行命令。
require('../math') 报 Cannot find module :相对路径是相对测试文件的,test/math.test.js 引用上级用 ../math;核对文件名。
ESM 项目里 require is not defined :package.json 有 "type": "module" 时测试文件也用 import:import test from 'node:test'、import assert from 'node:assert/strict'。
测试全部卡住不结束 :某个用例开了定时器/服务没关闭(server.listen、setInterval)。用例结束时清理资源,或 --test-force-exit 强制收尾再排查根因。
本地过、CI 挂 :多半是时区、路径分隔符或端口占用的环境差异。测试数据里别写死本地路径,监听端口用 0(系统随机分配)。
面试常问
单元测试的 AAA 模式是什么?
Arrange 准备输入与依赖、Act 执行被测行为、Assert 断言结果。每个用例聚焦一个行为、相互独立、描述可读。这样失败时能立刻定位到具体行为,用例本身就是活文档。
怎么测试异步函数和错误场景?
测试函数声明为 async 并 await 被测调用;错误场景同步用 assert.throws(() => fn()),异步用 await assert.rejects(() => fn()),可附带类型与 message 匹配。关键是别丢失 Promise(不 await 不 catch 就是假测试)。
覆盖率是什么?100% 意味着什么?
行/分支/函数三个维度统计测试执行到的代码比例。100% 只说明所有代码被执行过,不等于验证了正确行为;覆盖率用于发现遗漏(未覆盖模块),不是质量承诺。
node:test 相比 Jest 的取舍?
零依赖、官方维护、启动快,适合库/脚本/中小服务;Jest 生态在 mock、快照、React 组件测试上更成熟。核心概念(describe/it/断言/覆盖率)相通,迁移成本低。
自测三问
AAA 三个字母分别代表什么?一个用例里应包含几组 AAA?
测「divide(1,0) 会抛 RangeError」和「sumRemote 会 reject」,分别用什么断言?
assert.throws(divide(1,0)) 和 assert.throws(() => divide(1,0)) 有什么本质区别?
小结一句话 node:test 让测试零成本起步:node --test 发现用例、assert 断言成败、AAA 组织结构、覆盖率找漏——先红后绿,才是可信的测试。
延伸