-
-
Notifications
You must be signed in to change notification settings - Fork 31
Open
Description
Description
Since the tap test runner can be replaced by the built-in node:test module available in Node.js v18 and later, we should provide a codemod to migrate existing tests.
- The codemod should replace
tapimports/requires withnode:testandnode:assert/strictequivalents. - The codemod should convert
t.test()blocks totest()while preserving names, options, and async handling. - The codemod should map
tapassertion helpers (t.equal,t.same,t.ok,t.rejects, etc.) toassert/strictequivalents. - The codemod should migrate hook APIs (
t.before,t.after,t.beforeEach,t.afterEach,t.teardown) totest.before/afterand per-test cleanup viat.after(). - The codemod should transform tap-specific flags (
only,skip,todo,timeout) to theirnode:testcounterparts. - The codemod should update nested subtests and callback-style tests (
t.end()) to thenode:testpattern. - The codemod should adjust
t.plan()usage to thenode:testcontext plan while keeping assertion counts intact.
Important points
node:testships with Node.js (stable since v20) and uses thenode:assert/strictAPI for assertions by default.- Test discovery is handled by
node --testinstead of thetapCLI; suite-level configuration (reporters, coverage) can be updated in a follow-up workflow step that edits package.json or config files. - The
tobject fromnode:testexposes similar helpers (test,plan,after,diagnostic) but not tap-specific reporters or snapshots. t.teardown()in tap maps tot.after()insidenode:testto register per-test cleanup.- Tap's
t.pass()ort.fail()should be replaced with explicit assertions (e.g.,assert.ok(true)orassert.fail(err)). - Snapshot features (
t.matchSnapshot) and tap plugins are out of scope and should be replaced manually or with third-party utilities (for now).
CLI migration guidance
- Replace npm/yarn scripts invoking
tap ...withnode --testplus equivalent flags:--test-only(tap--only),--test-name-pattern(tap--grep),--test-timeout(tap--timeout),--test-concurrency(tap--jobs), and--test-reporter/--test-reporter-destination(tap reporters and--reporter-file). - Translate tap's include/exclude globs (
--include/--exclude,--files) tonode --testfile globs or test runner patterns; remove.taprc/package.json"tap" config blocks once mapped. - Coverage and reporter flags (
--coverage-report,--output-file/dir) are not provided bynode:test; usec8 node --testor external reporters as a separate workflow step. - Plugin-driven flags (filter/only, snapshot, fixture, typescript) have no direct
node:testanalog and should be replaced manually or with dedicated tooling.
Examples
Example 1: Basic CommonJS test
Before:
const t = require("tap");
function add(a, b) {
return a + b;
}
t.test("adds numbers", t => {
t.equal(add(1, 2), 3);
t.end();
});After:
const { test } = require("node:test");
const assert = require("node:assert/strict");
function add(a, b) {
return a + b;
}
test("adds numbers", t => {
assert.strictEqual(add(1, 2), 3);
t.end();
});Example 2: ESM with async assertions
Before:
import t from "tap";
import { fetchUser } from "./user.js";
t.test("loads user", async t => {
const user = await fetchUser(7);
t.same(user, { id: 7, active: true });
});After:
import test from "node:test";
import assert from "node:assert/strict";
import { fetchUser } from "./user.js";
test("loads user", async t => {
const user = await fetchUser(7);
assert.deepStrictEqual(user, { id: 7, active: true });
});Example 3: Global setup and teardown hooks
Before:
const t = require("tap");
const db = createDatabase();
t.before(async () => {
await db.connect();
});
t.after(async () => {
await db.close();
});
t.test("reads records", async t => {
const rows = await db.list();
t.ok(rows.length > 0);
});After:
import test from "node:test";
import assert from "node:assert/strict";
const db = createDatabase();
test.before(async () => {
await db.connect();
});
test.after(async () => {
await db.close();
});
test("reads records", async t => {
const rows = await db.list();
assert.ok(rows.length > 0);
});Example 4: Per-test setup with context
Before:
const t = require("tap");
const { openConnection } = require("./db");
t.beforeEach(async t => {
t.context.conn = await openConnection();
});
t.afterEach(async t => {
await t.context.conn.close();
});
t.test("stores item", async t => {
const saved = await t.context.conn.save({ id: 1 });
t.ok(saved.id);
});After:
const test = require("node:test");
const assert = require("node:assert/strict");
const { openConnection } = require("./db");
test.beforeEach(async t => {
t.context.conn = await openConnection();
});
test.afterEach(async t => {
await t.context.conn.close();
});
test("stores item", async t => {
const saved = await t.context.conn.save({ id: 1 });
assert.ok(saved.id);
});Example 5: Nested subtests
Before:
const t = require("tap");
t.test("api", t => {
t.test("GET /status", async t => {
const res = await requestStatus();
t.equal(res.statusCode, 200);
});
t.end();
});After:
const { test } = require("node:test");
const assert = require("node:assert/strict");
test("api", async t => {
await t.test("GET /status", async t => {
const res = await requestStatus();
assert.strictEqual(res.statusCode, 200);
});
});Example 6: Focused, skipped, and todo tests
Before:
const t = require("tap");
t.test("skipped", { skip: true }, t => {
t.pass("not run");
t.end();
});
t.only("focus this", async t => {
await doWork();
});
t.todo("future case", async t => {
await maybeImplement();
});After:
const test = require("node:test");
test("skipped", { skip: true }, t => {
t.end();
});
test.only("focus this", async t => {
await doWork();
});
test.todo("future case", async t => {
await maybeImplement();
});Example 7: Promise rejection assertions
Before:
import t from "tap";
import { failing } from "./service.js";
await t.rejects(failing(), { message: /boom/ });
await t.resolves(Promise.resolve("ok"));After:
import assert from "node:assert/strict";
import { failing } from "./service.js";
await assert.rejects(failing(), /boom/);
await assert.doesNotReject(Promise.resolve("ok"));Example 8: Teardown per test
Before:
const t = require("tap");
const server = createServer();
t.test("serves", t => {
t.teardown(() => server.close());
t.equal(server.listening, true);
t.end();
});After:
const { test } = require("node:test");
const assert = require("node:assert/strict");
const server = createServer();
test("serves", t => {
t.after(() => server.close());
assert.strictEqual(server.listening, true);
t.end();
});Example 9: Plan-based tests
Before:
const t = require("tap");
t.test("makes three assertions", t => {
t.plan(3);
t.ok(true);
t.equal(1 + 1, 2);
t.same({ a: 1 }, { a: 1 });
});After:
const { test } = require("node:test");
const assert = require("node:assert/strict");
test("makes three assertions", t => {
t.plan(3);
assert.ok(true);
assert.strictEqual(1 + 1, 2);
assert.deepStrictEqual({ a: 1 }, { a: 1 });
});Example 10: Callback-style tests
Before:
const t = require("tap");
t.test("uses callback", t => {
setTimeout(() => {
t.ok(true);
t.end();
}, 10);
});After:
const { test } = require("node:test");
const assert = require("node:assert/strict");
test("uses callback", t => {
setTimeout(() => {
assert.ok(true);
t.end();
}, 10);
});Example 11: package.json script migration
Before:
{
"scripts": {
"test": "tap",
"test:watch": "tap --watch",
"coverage": "tap --coverage-report=lcov"
}
}After:
{
"scripts": {
"test": "node --test",
"test:watch": "node --test --watch",
"coverage": "c8 node --test"
}
}Caveats
- Tap-specific reporters, coverage integration, and snapshot helpers are not provided by
node:test; equivalent functionality must be handled by external tools. - Tap plugins and custom assertions may require manual rewrites or replacement libraries..
Refs
Metadata
Metadata
Assignees
Labels
No labels
Type
Projects
Status
๐ Backlog