前端知库

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 监听接口方法,检查调用次数:

JavaScript
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 展开:

bash
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 按名字过滤。