nodejs / nodejs/userland-migrations
spec: `tap-to-node-test`
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 85
- Forks
- 55
- Avg merge
- 3d 11h
- Merged PRs (30d)
- 9
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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
The issue names no existing files, tests, or codemod entry points. Start by locating the repository's codemod structure and test conventions, then define coverage for the listed Tap-to-node:test transformations; done means supported imports, assertions, hooks, flags, nested tests, callbacks, plans, and documented out-of-scope cases are handled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js, typescript
- Domain
- testing-qa, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100