← zlib 模块 V8 模块 →

Node.js assert断言测试:从assert.throws到node:test的单元测试写法

著
原创 2026-10-13 Node.js 已有人查阅

一、断言测试解决的是什么问题

写业务代码时,一个函数写完,开发者通常会在脑子里过一遍:传这个参数应该返回那个结果。这种"过一遍"就是最原始的验证。问题在于,人脑的验证不可重复,改了几行代码之后,之前验证过的逻辑可能已经被破坏,而开发者未必记得再跑一遍。

断言测试把这种验证固化下来。它的基本形式是一句判断:实际拿到的值,和预期的值是否一致。一致就通过,不一致就抛出AssertionError,中断执行并告知哪里的预期落空了。Node.js把这类判断能力内置到了assert模块,不需要引入Jest、Mocha这类测试框架就能用。

需要区分一个概念:assert模块本身只是"判断工具",它不负责组织测试、不负责生成报告。真正让测试跑起来的是node:test这样的测试运行器。两者配合使用,才是完整的单元测试方案。

二、assert模块的导入与基本判定

Node.js推荐用node:前缀导入内置模块:

const assert = require('node:assert');

也可以导入严格模式版本:

const assert = require('node:assert/strict');

两者的区别在于:严格模式下,equal等价于strictEqual,deepEqual等价于deepStrictEqual。项目里我倾向于直接用严格模式,理由是松散比较的隐式类型转换在测试场景中弊大于利——测试要的是精确判定,不是"差不多就行"。

三、常用断言方法逐一说明

assert模块的方法可以按判定对象分成几组。

真值判定组:assert()和assert.ok()是同一个函数,判断传入值是否为真值。assert.ifError()逻辑相反,传入真值时抛错,传入null或undefined时静默通过。ifError在回调风格的代码里比较实用,用来断言err参数为空。

相等判定组:equal用==比较,notEqual用!=比较,这两个是松散比较。strictEqual用===,notStrictEqual用!==,这两个是严格比较。日常写测试,strictEqual的使用频率远高于equal。

深度判定组:deepEqual和deepStrictEqual用于比较对象或数组的内部结构。区别同样在于是否做类型严格校验。deepStrictEqual会检查原型链和类型,deepEqual不会。

异常判定组:throws验证函数是否抛出错误,doesNotThrow验证函数不抛错。异步场景对应rejects和doesNotReject。

主动失败组:fail()直接抛出AssertionError,通常用在switch的default分支或理论上不该到达的代码路径。

方法 判定规则 典型用途
assert.ok(value) value为真值则通过 通用真值校验
assert.strictEqual(a,b) a===b则通过 基本类型精确比较
assert.notStrictEqual(a,b) a!==b则通过 确认两值不同
assert.deepStrictEqual(a,b) 结构相同且类型严格一致 对象、数组比较
assert.throws(fn,error) fn抛出匹配error的异常 校验同步报错逻辑
assert.rejects(asyncFn,error) Promise以匹配error拒绝 校验异步报错逻辑
assert.ifError(err) err为假值则通过 回调错误参数检查
assert.fail(msg) 无条件抛出AssertionError 标记不可达分支

四、assert.throws的参数结构与匹配规则

throws的完整签名是assert.throws(fn[,error][,message])。

第一个参数fn是待验证的函数,assert会调用它并捕获抛出的异常。第二个参数error是可选参数,用来描述"期望的错误长什么样",它可以是构造函数、正则表达式、校验函数或对象。第三个参数message是断言失败时附加的提示信息。

const assert = require('node:assert');

function parseAge(input) {
    const age = Number(input);
    if (Number.isNaN(age)) {
        throw new TypeError('年龄必须是数字');
    }
    return age;
}

assert.throws(
    () => parseAge('abc'),
    TypeError
);

console.log('类型错误校验通过');

error传TypeError时,assert会检查抛出的异常是否为TypeError的实例。如果传正则表达式,则匹配错误消息文本:

assert.throws(
    () => parseAge('abc'),
    /年龄必须是数字/
);

两种写法各有适用场景。传构造函数适合校验错误类型,传正则适合校验错误消息内容。项目里如果错误消息会随版本调整,用构造函数更稳定;如果错误类型统一但需要区分具体原因,用正则更精确。

这里有个细节值得注意:throws返回的是被抛出的错误对象。也就是说可以把它赋值给变量,后续继续检查错误的属性:

const err = assert.throws(() => parseAge('abc'), TypeError);
console.log(err.message);

五、assert.rejects与异步错误校验

异步函数抛出的错误不会同步传播,throws捕获不到。这时要用rejects。

const assert = require('node:assert');

async function fetchUser(id) {
    if (!id) {
        throw new Error('用户 ID 不能为空');
    }
    return { id, name: '代码号学习编程' };
}

async function runTest() {
    await assert.rejects(
        () => fetchUser(''),
        /用户 ID 不能为空/
    );
    console.log('异步错误校验通过');
}

runTest();

rejects的第一个参数可以是Promise,也可以是返回Promise的函数。传函数的好处是assert会负责调用它并处理同步抛错的情况。如果直接传一个已经rejected的Promise,且在传入之前没有处理拒绝,Node.js可能触发unhandledRejection警告。所以更稳妥的写法是传函数。

rejects返回的是一个Promise,必须用await等待,否则断言结果不会被正确捕获。这一点在写测试时容易漏掉,漏掉的表现是测试"通过了"但实际上什么都没校验。

六、配合node:test组织测试用例

assert负责判断,node:test负责组织。node:test模块提供了test()函数来定义测试用例。

const test = require('node:test');
const assert = require('node:assert');

function add(a, b) {
    return a + b;
}

test('代码号学习编程 - 加法函数测试', () => {
    assert.strictEqual(add(2, 3), 5);
});

把这段代码保存为add.test.js,在命令行执行node--testadd.test.js,会看到TAP格式的输出,包含测试名称、通过状态和耗时。TAP是TestAnythingProtocol的缩写,一种跨语言的测试报告格式。

测试用例可以嵌套,用t.test()或者describe/it风格组织:

const test = require('node:test');
const assert = require('node:assert');

test('用户模块', async (t) => {
    await t.test('ID 为空时应该报错', async () => {
        await assert.rejects(
            () => fetchUser(''),
            /用户 ID 不能为空/
        );
    });

    await t.test('正常 ID 返回用户对象', async () => {
        const user = await fetchUser(1);
        assert.strictEqual(user.name, '代码号学习编程');
    });
});

这种嵌套结构在测试报告里会体现出层级关系,出问题时能快速定位是哪个子场景失败。

七、经验分享:strictEqual与deepStrictEqual的选型

项目里经常看到两种写法混用的情况。我的建议是按数据类型分:

基本类型(数字、字符串、布尔、null、undefined)用strictEqual。对象和数组用deepStrictEqual。原因很直接:对象用strictEqual比较的是引用地址,两个内容相同的对象会判定为不相等,这不是测试想要的结果。

const assert = require('node:assert');

assert.deepStrictEqual(
    { name: '代码号学习编程', level: 1 },
    { name: '代码号学习编程', level: 1 }
);

deepStrictEqual会递归比较每个属性的值和类型。如果对象里嵌套了Date、RegExp这类特殊对象,它也能正确比较。但要注意,deepStrictEqual不比较原型链上的方法,只比较自有属性。

另一个经验是:不要用deepEqual(松散版本)。松散深度比较在遇到1和'1'时判定为相等,这在项目里往往是bug而不是预期行为。严格模式下的deepStrictEqual能避免这类误判。

八、错误处理与断言消息

断言失败时抛出的AssertionError会包含actual、expected、operator等属性,测试运行器会把这些信息格式化输出。如果觉得默认信息不够明确,可以在方法末尾加message参数:

assert.strictEqual(
    add(2, 3),
    5,
    '加法函数返回值不符合预期'
);

这条消息会出现在失败报告中,帮助定位问题。在参数化测试或多场景测试里,加上有意义的message能省不少排查时间。

还有一点:assert的断言失败是抛异常,不是返回值。所以它不能被if包裹,也不能用try/catch吞掉后当作通过。断言失败就应该让测试失败,这是它的职责。

九、本节课程知识要点

  • assert模块是Node.js内置模块,用require('node:assert')导入,严格模式用node:assert/strict

  • strictEqual用于基本类型精确比较,deepStrictEqual用于对象和数组结构比较

  • throws校验同步函数抛错,rejects校验异步函数或Promise拒绝,两者都支持构造函数、正则、对象、校验函数四种匹配方式

  • rejects第一个参数建议传函数而非已创建的Promise,避免unhandledRejection

  • rejects返回Promise,必须await,否则断言不生效

  • 配合node:test的test()函数组织用例,用node--test运行,输出TAP格式报告

  • 断言失败抛AssertionError,不能也不应该被catch后忽略

十、练习与思考

  1. 写一个divide(a,b)函数,当b为0时抛出RangeError,用assert.throws校验这一行为,并额外用正则匹配错误消息。

  2. 写一个返回Promise的fetchConfig()函数,在配置缺失时reject,用assert.rejects校验,并尝试传构造函数和正则两种error参数,观察差异。

  3. 用node:test为一个包含三个方法的工具模块写测试,其中至少有一个测试用deepStrictEqual比较返回的对象。

  4. 思考:如果某个断言在循环里反复执行,失败时如何让报告能定位到是第几次迭代出的问题?

assert模块的API数量不多,但每个方法的判定规则有细微差别,尤其是松散比较和严格比较、同步校验和异步校验这两组区分。实际项目里,先把strictEqual、deepStrictEqual、throws、rejects这四个用熟,配合node:test就能覆盖大部分单元测试场景。遇到断言结果不符合直觉时,先确认用的是不是严格版本,再检查比较的数据类型是否一致,多数问题都出在这两点上。

← zlib 模块 V8 模块 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号