如果希望优雅关闭(graceful shutdown)和其他测试一起运行,就用 checkShutdown()。这个函数会自己启动并清理服务,所以测试里不需要另外编写服务器的 setup 和 teardown。
用 node:test 测试
import assert from "node:assert/strict";
import { test } from "node:test";
import { checkShutdown, defineConfig } from "shutdown-check";
const config = defineConfig({
command: ["node", "server.js"],
env: { PORT: "3510" },
baseUrl: "http://127.0.0.1:3510",
readiness: { path: "/health", timeoutMs: 10_000 },
workload: {
path: "/slow",
bodyIncludes: "work complete",
started: { type: "response-headers" },
},
shutdown: {
deadlineMs: 10_000,
readinessWithdrawal: true,
},
});
test("drains active requests on SIGTERM", { timeout: 30_000 }, async (t) => {
const result = await checkShutdown(config);
if (!result.pass) {
for (const item of result.timeline) {
const detail = item.detail ? ` — ${item.detail}` : "";
t.diagnostic(`+${item.ms} ms ${item.event}${detail}`);
}
if (result.stderr) t.diagnostic(result.stderr);
if (result.stdout) t.diagnostic(result.stdout);
}
assert.equal(result.pass, true, `${result.code}: ${result.message}`);
});运行这个文件:
node --test test/shutdown.test.mjs断言消息会把固定不变的诊断码和说明写进测试失败信息里。诊断信息(diagnostic)只在需要时才补充时间线和服务输出。
用 Vitest 测试
import { checkShutdown, defineConfig } from "shutdown-check";
import { expect, test } from "vitest";
const config = defineConfig({
command: ["node", "server.js"],
env: { PORT: "3511" },
baseUrl: "http://127.0.0.1:3511",
readiness: { path: "/health", timeoutMs: 10_000 },
workload: {
path: "/slow",
bodyIncludes: "work complete",
started: { type: "response-headers" },
},
shutdown: {
deadlineMs: 10_000,
readinessWithdrawal: true,
},
});
test("drains active requests on SIGTERM", async () => {
const result = await checkShutdown(config);
if (!result.pass) {
console.error(result.timeline);
if (result.stderr) console.error(result.stderr);
if (result.stdout) console.error(result.stdout);
}
expect(result.pass, `${result.code}: ${result.message}`).toBe(true);
}, 30_000);运行:
npx vitest run test/shutdown.test.tsVitest 的默认超时通常太短,不够跑完一个真实进程的完整生命周期。请在测试上传入超时,或在 Vitest 配置中设置超时。
选择测试超时
外层测试的超时必须大于:
readiness.timeoutMs
+ workload.started.timeoutMs
+ shutdown.deadlineMs
+ 进程本身的额外开销使用默认值时,把测试运行器的超时设为 30 秒比较合理。这并不意味着每次运行都要 30 秒;正常情况下,服务一退出,运行就结束了。
如果测试运行器先超时,shutdown-check 的结果和时间线可能就看不到了。外层超时要给得宽松些,而关闭的截止时间要贴近生产环境的真实限制。
断言真正有用的值
即使检查发现了关闭问题(即预期内的失败),checkShutdown() 返回的 Promise 也会正常 resolve。所以要断言 result.pass,不能只看 Promise 有没有 resolve。
assert.equal(result.pass, true, `${result.code}: ${result.message}`);如果是反向测试,用来证明有问题的服务器会被检测出来,就断言诊断码:
assert.equal(result.code, "SC311");除非完整的文本本身就是你要测试的内容,否则不要匹配完整的消息。诊断码才是稳定的失败标识。
处理无效配置
输入无效时,checkShutdown() 会在检查开始前以一个 Error reject。
await assert.rejects(
() => checkShutdown(invalidConfig),
/baseUrl must use http:\/\//
);defineConfig() 可以让编辑器给出提示,但要记住它不做运行时校验。校验是在调用 checkShutdown() 时进行的。
使用 CommonJS
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { checkShutdown } = require("shutdown-check");
test("drains active requests", { timeout: 30_000 }, async () => {
const result = await checkShutdown({
command: ["node", "server.js"],
env: { PORT: "3512" },
baseUrl: "http://127.0.0.1:3512",
readiness: { path: "/health" },
workload: {
path: "/slow",
started: { type: "response-headers" },
},
});
assert.equal(result.pass, true, `${result.code}: ${result.message}`);
});shutdown-check 1.0.1 及以上版本支持 CommonJS。
给每个测试分配独立端口
测试运行器可能并行执行多个文件。每次检查都要求端口在启动前处于空闲状态,所以两个关闭检查不能共用同一个端口。
使用不同的端口:
const ordersConfig = defineConfig({
env: { PORT: "3510" },
baseUrl: "http://127.0.0.1:3510",
// ...
});
const billingConfig = defineConfig({
env: { PORT: "3511" },
baseUrl: "http://127.0.0.1:3511",
// ...
});数据库记录、文件、队列和其他状态也要相互隔离。shutdown-check 运行的是真实的服务,发送的也是真实的请求。
可靠地解析路径
默认情况下,相对路径基于 process.cwd() 解析。测试运行器通常以项目根目录作为工作目录,但在 workspace 或自定义脚本中可能会变。
显式传入一个基准目录:
const result = await checkShutdown(config, import.meta.dirname);这样,相对的 cwd 值就会基于测试文件所在的目录解析。
避免测试失败后遗留进程
shutdown-check 在 resolve 之前,会清理它发出的测试请求、定时器和服务的进程组。不要在 beforeAll 钩子里再启动同一个服务器,进程的生命周期由检查自己管理。
如果测试运行器在 shutdown-check resolve 之前强制停止了测试,清理可能来不及完成。这也是外层超时要长于检查自身各项超时的另一个原因。