CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Node.js 后端入门/课时/测试 node:test
RawBlame
课时目录12 节

Node.js 后端入门

  1. 01Node.js 是什么
  2. 02运行时与 process
  3. 03模块与 npm
  4. 04文件系统与 path
  5. 05http 模块
  6. 06Express 入门
  7. 07事件循环与异步
  8. 08环境变量与配置
  9. 09Streams 基础
  10. 10错误处理
  11. 11测试 node:test
  12. 12综合练习与选型

测试 node:test

内置测试运行器、断言、HTTP 接口测试入门。

30 分钟

测试 node:test

这一课你将能做到

  1. 用 Node 20 内置的 node:test 写出单元测试并运行,全程零第三方依赖
  2. 使用 assert 断言普通值、抛错(assert.throws)和异步拒绝(assert.rejects)
  3. 用 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"
}

以后就是标准的 npm 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: '网络错误' }
    );
  });
});
node --test

异步用例两条规则:测试函数写成 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"
}

讲清楚要点

  1. node:test 是 Node 20 内置的稳定测试运行器:node --test 自动发现 test/ 目录和 *.test.js 文件;断言用同样内置的 node:assert,一条第三方依赖都不用装。
  2. AAA 是用例的组织纪律:Arrange 准备数据、Act 执行被测函数、Assert 断言结果。一个用例只验证一个行为,描述写成「能当文档读」的句子。
  3. 每个用例必须独立:不依赖执行顺序、不共享可变状态。测试文件之间并行执行,共享可变状态会导致「单独跑能过、一起跑就炸」。
  4. 同步用 assert.throws,异步用 assert.rejects:Promise 拒绝不会触发同步 try/catch,测异步错误必须用 rejects + await。
  5. 失败优先原则:新写测试先让它们红一次,确认断言真的在工作;只见过绿的测试可能从第一行起就是假测试。

你可能会懵的地方

  • 不用 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

卡点急救

  1. node --test 说找不到测试:确认文件名匹配 *.test.js 或位于 test/ 目录;在项目根目录运行命令。
  2. require('../math') 报 Cannot find module:相对路径是相对测试文件的,test/math.test.js 引用上级用 ../math;核对文件名。
  3. ESM 项目里 require is not defined:package.json 有 "type": "module" 时测试文件也用 import:import test from 'node:test'、import assert from 'node:assert/strict'。
  4. 测试全部卡住不结束:某个用例开了定时器/服务没关闭(server.listen、setInterval)。用例结束时清理资源,或 --test-force-exit 强制收尾再排查根因。
  5. 本地过、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/断言/覆盖率)相通,迁移成本低。

自测三问

  1. AAA 三个字母分别代表什么?一个用例里应包含几组 AAA?
  2. 测「divide(1,0) 会抛 RangeError」和「sumRemote 会 reject」,分别用什么断言?
  3. assert.throws(divide(1,0)) 和 assert.throws(() => divide(1,0)) 有什么本质区别?

小结一句话

node:test 让测试零成本起步:node --test 发现用例、assert 断言成败、AAA 组织结构、覆盖率找漏——先红后绿,才是可信的测试。

延伸

  • Node.js 官方文档 test runner
  • Node.js assert 文档
  • 下一课:综合练习与选型——把 12 课的积木拼成一个真实小项目,并回答「什么时候用 Node、什么时候用 Java」。
加载本节练习…
← 上一课错误处理下一课 →综合练习与选型