Use checkShutdown() when graceful shutdown should run with the rest of your
tests. The function starts and cleans up the service, so the test needs no
separate server setup or teardown.
Test with 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}`);
});Run the file:
node --test test/shutdown.test.mjsThe assertion message keeps the stable code and explanation in the test failure. Diagnostics add the timeline and service output only when needed.
Test with 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);Run:
npx vitest run test/shutdown.test.tsVitest's default timeout is normally too short for a real process lifecycle, so pass a timeout to the test or set one in the Vitest config.
Choose the test timeout
The outer test timeout must exceed:
readiness.timeoutMs
+ workload.started.timeoutMs
+ shutdown.deadlineMs
+ process overheadFor defaults, 30 seconds is a reasonable test-runner timeout. This does not mean every run takes 30 seconds; a normal run ends when the service exits.
If the test runner times out first, it may hide the shutdown-check result and timeline. Keep the outer timeout generous while keeping the shutdown deadline close to the real production limit.
Assert the useful value
checkShutdown() resolves after an expected shutdown failure. Assert
result.pass, not only whether the promise resolves.
assert.equal(result.pass, true, `${result.code}: ${result.message}`);For a negative test that proves a broken server is detected, assert the code:
assert.equal(result.code, "SC311");Avoid matching the full message unless the exact text is part of your test. The diagnostic code is the stable failure identifier.
Handle invalid config
Invalid input causes checkShutdown() to reject with an Error before the
test runs.
await assert.rejects(
() => checkShutdown(invalidConfig),
/baseUrl must use http:\/\//
);Use defineConfig() for editor feedback, but remember that it does not perform
runtime validation. checkShutdown() validates when called.
Use 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}`);
});CommonJS support is available in shutdown-check 1.0.1 and later.
Give every test its own port
Test runners may execute files in parallel. Two shutdown checks cannot share a port because each run requires the port to be free before launch.
Use separate ports:
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",
// ...
});Also isolate database rows, files, queues, and other state. shutdown-check runs the actual service and sends real requests.
Resolve paths reliably
By default, relative paths resolve from process.cwd(). Test runners usually
use the project root, but this can change in workspaces or custom scripts.
Pass a base directory explicitly:
const result = await checkShutdown(config, import.meta.dirname);Then relative cwd values are resolved from the test file's directory.
Avoid leaked processes after a test failure
shutdown-check cleans up its workload requests, timers, and service process
group before resolving. Do not start the same server in a beforeAll hook;
the check owns the process lifecycle.
If a test runner force-stops the test before shutdown-check resolves, cleanup may not finish. This is another reason to use an outer timeout longer than the check's own timeouts.