# Tests with node:test or Vitest

> Test graceful shutdown with node:test or Vitest: call checkShutdown from a test, set a long enough timeout, assert the result and print the timeline.

Source: https://shutdown.jscrate.dev/docs/guides/test-runners
Last updated: 2026-09-23

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

```js title="test/shutdown.test.mjs"
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:

```sh
node --test test/shutdown.test.mjs
```

The 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

```ts title="test/shutdown.test.ts"
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:

```sh
npx vitest run test/shutdown.test.ts
```

Vitest'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:

```text
readiness.timeoutMs
+ workload.started.timeoutMs
+ shutdown.deadlineMs
+ process overhead
```

For 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.

```js
assert.equal(result.pass, true, `${result.code}: ${result.message}`);
```

For a negative test that proves a broken server is detected, assert the code:

```js
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.

```ts
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

```js title="test/shutdown.test.cjs"
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:

```ts
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:

```js
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.

## Related

- [Node API](https://shutdown.jscrate.dev/docs/node-api)
- [Configuration reference](https://shutdown.jscrate.dev/docs/configuration)
- [Output and reports](https://shutdown.jscrate.dev/docs/output)
- [Run in CI](https://shutdown.jscrate.dev/docs/ci)
- [Quick start](https://shutdown.jscrate.dev/docs/quick-start)
