Node.js 内置测试运行器入门指南
Node.js 18 起内置测试运行器,无需安装任何第三方依赖即可写测试。本文讲解 node:test 的编写与运行、describe/it 语法、用例过滤、Mock、钩子、覆盖率与测试报告。
本文依据官方文档整理,未执行运行验证或性能基准。代码片段展示局部用法,业务函数、数据和环境需按项目补齐;版本与配置以所引文档为准。
直接回答:Node.js 从 v18 开始内置了官方测试运行器(node:test),无需安装 Jest、Mocha 等外部依赖就能编写、运行测试,并自带 Mock、钩子、覆盖率与报告能力——Node 项目的"零依赖测试"时代正式到来。
背景
长期以来 Node.js 没有官方测试方案,社区形成了 Jest、Mocha、Tap 等第三方生态。后来核心团队将测试运行器并入 Node 本体,v18 引入,v20 的核心运行器稳定;mock、快照、覆盖率等子功能各自有版本和稳定性边界。对于想减少依赖的项目,这是当下最值得关注的默认选项。
第一个测试
本文按支持这些API的现代Node版本演示;先在 package.json 设置 "type": "module"(或改用 .mjs)。新建 math.js:
export function add(a, b) {
return a + b;
}
再建 math.test.js:
import { test } from "node:test";
import assert from "node:assert/strict";
import { add } from "./math.js";
test("add 函数", async (t) => {
await t.test("1 + 2 等于 3", () => {
assert.equal(add(1, 2), 3);
});
await t.test("负数相加", () => {
assert.equal(add(-1, -1), -2);
});
});
运行:
node --test
Node 会自动发现 *.test.js 等约定命名的文件并执行。
describe/it 语法(可选)
习惯 BDD 风格的话,内置 runner 也提供:
import { describe, it } from "node:test";
describe("add", () => {
it("1 + 2 = 3", () => {
assert.equal(add(1, 2), 3);
});
});
node:assert/strict 是推荐断言库——assert.equal 在 strict 模式下就是严格相等,避免传统 assert 的隐式转换坑。
过滤与限定
node --test math.test.js:指定文件;node --test --test-name-pattern="负数":按名称过滤;- 代码里
t.test("...", { only: true }, ...)配合node --test --test-only:只跑标记用例; node --test --watch:监视模式。
Mock 能力
内置 runner 自带 mock 工具:
import { mock } from "node:test";
const mockedFetch = mock.fn(async () => ({ ok: true }));
const request = async (fetcher) => fetcher('https://example.test');
await request(mockedFetch);
assert.equal(mockedFetch.mock.callCount(), 1);
mock.method(obj, "methodName") 可以替换对象方法,mock.timers 还能接管定时器做时间相关测试。
钩子
test("套件", async (t) => {
t.before(() => { /* 套件前 */ });
t.beforeEach(() => { /* 每条前 */ });
t.afterEach(() => { /* 每条后 */ });
t.after(() => { /* 套件后 */ });
});
覆盖率与报告
node --test --experimental-test-coverage
直接输出行/分支/函数覆盖率。报告方面,当前默认报告器取决于是否连接TTY及版本,不应假定总是TAP;可显式指定 --test-reporter=tap,也可以通过 --test-reporter=spec 获得更易读的格式,或输出 JUnit XML 供 CI 展示。
一个简单的 HTTP 服务测试
import { test } from "node:test";
import assert from "node:assert/strict";
import { createServer } from "node:http";
import { once } from "node:events";
test("服务器返回 200", async (t) => {
const server = createServer((req, res) => {
res.writeHead(200).end("ok");
});
t.after(() => new Promise((resolve, reject) => {
server.close(err => err ? reject(err) : resolve());
server.closeAllConnections();
}));
server.listen(0, "127.0.0.1");
await once(server, "listening");
const port = server.address().port;
const res = await fetch(`http://localhost:${port}/`);
assert.equal(res.status, 200);
assert.equal(await res.text(), "ok");
});
端口传 0 让系统分配,避免并行测试撞端口。
常见问题(FAQ)
Q:内置 runner 能完全替代 Jest 吗?
A:对大多数 Node 后端项目:可以。前端组件快照、jsdom 环境等仍是 Jest/Vitest 的主场。后端 API、库、CLI 工具用内置 runner 已经非常顺手。
Q:TypeScript 项目怎么用?
A:Node 的内置类型剥离支持与默认开关随版本变化,且不做类型检查、不读取 tsconfig 路径转换;也可使用兼容的 tsx 加载方案。生产项目建议还是先编译再测,保持与线上产物一致。
Q:并行执行怎么控制?
A:默认测试文件间并行、文件内顺序执行。--test-concurrency 调整并行度;同一文件内的子测试默认串行,需要并发时显式设置 concurrency: true。
官方参考
资料核对日期:2026-09-29。