node:test 能替代 Jest 吗?
node:test 是 Node.js 内置的测试运行器,v20 起稳定,不用装依赖就有 test/describe/it、钩子、函数和定时器 mock、快照、多种报告器,node --test 自动发现测试文件。纯 Node 的库和服务端代码可以用它替代 Jest;前端组件测试需要 jsdom 环境、模块 mock 和成熟生态,仍然是 Jest 或 Vitest 更合适。覆盖率和模块 mock 目前还是实验性功能。
简答
比如写一个 Node 的工具库,只想测几个纯函数和一个调接口的模块,不想为此装 Jest 和一堆配置。Node.js 自带了测试运行器
node:test,v18 加入、v20 起稳定,写好测试文件运行node --test就行。它覆盖了单元测试的基本需求:
test、describe、it和before、afterEach等钩子;mock.fn、mock.method能记录调用,mock.timers能模拟定时器和Date,通过测试上下文t.mock创建的 mock 在测试结束后自动还原;快照测试从 v23.4 起不再是实验性功能;报告器内置spec、tap、dot、junit和lcov。断言用node:assert。所以纯 Node 的库、CLI 和服务端代码,用它替代 Jest 是可行的,少一个依赖,也不需要转译。
可是前端项目一般不适合换。它没有内置 jsdom 这类浏览器环境,组件测试要的生态也在 Jest 和 Vitest 那边;模块 mock 要加
--experimental-test-module-mocks参数,还处于早期开发阶段,代码覆盖率要加--experimental-test-coverage,也是实验性的。只能用node:test这个带前缀的名字导入。
一个最小的测试文件
下面测一个购物车的合计函数,它会调用接口取单价。测试里用 t.mock.method 监听接口方法,检查调用次数:
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
function createCart(api) {
return {
async total(ids) {
const prices = await Promise.all(ids.map((id) => api.price(id)));
return prices.reduce((a, b) => a + b, 0);
},
};
}
describe('购物车', () => {
it('合计价格', async (t) => {
const api = { price: async (id) => (id === 'a' ? 10 : 5) };
t.mock.method(api, 'price');
const cart = createCart(api);
assert.equal(await cart.total(['a', 'b']), 15);
assert.equal(api.price.mock.callCount(), 2);
});
});
把它存成 cart.test.mjs,运行 node --test 就会执行,用 Node.js 22.23 实测通过。
读者可能会问,为什么不写成 import { test } from 'test'?因为这个模块只能通过 node: 前缀访问,不带前缀会去找 npm 上叫 test 的包。
t.mock 和顶层的 mock 用法一样,区别在于通过测试上下文创建的 mock,测试结束时会自动还原,不用手动 restore。
node --test 会找哪些文件
不传参数时,node --test 按约定匹配文件,包括 **/*.test.{cjs,mjs,js}、**/*-test.*、**/*_test.*、**/test-*.*、**/test.*,以及 test 目录下的所有脚本。当前文档里,在没有加 --no-strip-types 的情况下,.ts、.mts、.cts 后缀也会匹配。也可以自己传 glob,记得加引号,避免被 shell 展开:
node --test "**/*.spec.js"
--watch 可以监听测试文件和它的依赖,改动后重跑受影响的测试,这个功能目前是实验性的。默认每个测试文件在单独的子进程里运行。任何测试失败,进程退出码都会被设为 1,CI 能直接据此判断。
和 Jest 比还差什么
第一是浏览器环境。Jest 和 Vitest 都可以切到 jsdom 环境测组件,node:test 没有这一层,前端组件测试要自己接,或者干脆用 Vitest,见《前端单元测试怎么写?Jest 和 Vitest 有什么区别?》。
第二是模块 mock。Jest 的 jest.mock 很常用,node:test 对应的 mock.module 要加 --experimental-test-module-mocks 参数,稳定性标记是 1.0,也就是早期开发阶段。
第三是覆盖率。要加 --experimental-test-coverage 参数,同样是实验性的;可以配合 lcov 报告器输出给其他工具。
另外文档提醒,内置报告器的输出格式在不同 Node 版本之间可能变化,不要让程序去解析它。v23 起非 TTY 输出的默认报告器也从 tap 改成了 spec。
定时器测试本身容易出的问题,见《前端怎样测试定时器、重试和超时,避免 Fake Timers 造成假通过?》。
常见追问
t.test() 写的子测试和 describe 里的 it 有什么区别?
文档特别提醒过,用
t.test()创建的子测试,父测试不会自动等它完成,父测试结束时还没跑完的子测试会被取消并算作失败,所以要await t.test(...)。写在describe(也就是suite)里的测试没有这个问题。
快照测试怎样更新?
用
t.assert.snapshot(value)断言,第一次或需要更新时,带上--test-update-snapshots参数运行,快照默认写到测试文件旁边的.snapshot文件里。
只想跑某一个测试怎么办?
可以用
test.only、it.only,再用--test-only参数运行;test.skip跳过,test.todo标记待完成。也可以用--test-name-pattern按名字过滤。