nodejs / nodejs/userland-migrations
spec: `ava-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 ava 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
avaimports withnode:testandnode:assert/strictequivalents. - The codemod should convert
test()blocks totest()while preserving names, options, and async handling. - The codemod should map
avaassertion helpers (t.is,t.deepEqual,t.throws,t.truthy, etc.) toassert/strictequivalents. - The codemod should migrate hook APIs (
test.before,test.after,test.beforeEach,test.afterEach) to theirnode:testcounterparts. - The codemod should transform ava-specific flags (
serial,only,skip) to theirnode:testequivalents. - The codemod should update assertions that inspect error objects to use
assert.throws()and error validation with matcher functions. - The codemod should handle macro functions by inlining the test logic or converting to parameterized test patterns.
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 theavaCLI; suite-level configuration (reporters, concurrency, timeout) 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 ava-specific features like macros or snapshots. test.serialmaps tonode --test --test-concurrency=1or per-file serial markers; there is no per-test serial equivalent innode:test.- Ava's
t.throwsAsync()should be converted toassert.rejects()with error checking. - Ava's macro functions have no direct equivalent; test factories or parameterization must be handled manually.
- Snapshot features (
t.snapshot) and ava 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
ava ...withnode --testplus equivalent flags:--test-only(ava--matchwith exact match),--test-name-pattern(ava--match),--test-timeout(ava--timeout), and--test-concurrency(ava--concurrency). - Translate ava's file patterns (
filesconfig) to positional arguments or file globs passed tonode --test. - Coverage options (
.nyc_configor c8 setup) require external tools; usec8 node --testas a separate workflow step. - Ava's
--watchmode, custom reporters, and tap output are not built intonode:test; use external tools (nodemon, custom reporters) for equivalent functionality. - Remove
.avarc.json,ava.config.js, oravaconfig block from package.json once migration is complete.
Examples
Example 1: Basic test with assertions
Before:
import test from "ava";
function add(a, b) {
return a + b;
}
test("adds numbers", t => {
t.is(add(1, 2), 3);
});
After:
import { test } from "node:test";
import assert from "node:assert/strict";
function add(a, b) {
return a + b;
}
test("adds numbers", t => {
assert.strictEqual(add(1, 2), 3);
});
Example 2: Deep equality assertions
Before:
import test from "ava";
test("object equality", t => {
t.deepEqual({ a: 1 }, { a: 1 });
t.notDeepEqual({ a: 1 }, { a: 2 });
});
After:
import { test } from "node:test";
import assert from "node:assert/strict";
test("object equality", t => {
assert.deepStrictEqual({ a: 1 }, { a: 1 });
assert.notDeepStrictEqual({ a: 1 }, { a: 2 });
});
Example 3: Async tests and promises
Before:
import test from "ava";
test("async operation", async t => {
const result = await fetchData();
t.truthy(result);
});
After:
import { test } from "node:test";
import assert from "node:assert/strict";
test("async operation", async t => {
const result = await fetchData();
assert.ok(result);
});
Example 4: Error handling and throws
Before:
import test from "ava";
test("throws on invalid input", t => {
const error = t.throws(() => {
parseJSON("{invalid}");
});
t.is(error.message, "Unexpected token");
});
After:
import { test } from "node:test";
import assert from "node:assert/strict";
test("throws on invalid input", t => {
assert.throws(() => {
parseJSON("{invalid}");
}, /Unexpected token/);
});
Example 5: Async throws and rejects
Before:
import test from "ava";
test("rejects on error", async t => {
await t.throwsAsync(failingPromise(), { message: /timeout/ });
});
After:
import { test } from "node:test";
import assert from "node:assert/strict";
test("rejects on error", async t => {
await assert.rejects(failingPromise(), /timeout/);
});
Example 6: Global and per-test hooks
Before:
import test from "ava";
const db = createDatabase();
test.before(async t => {
await db.connect();
});
test.after.always(async t => {
await db.close();
});
test("queries database", async t => {
const result = await db.query("SELECT 1");
t.truthy(result);
});
After:
import { test } from "node:test";
import assert from "node:assert/strict";
const db = createDatabase();
test.before(async t => {
await db.connect();
});
test.after(async t => {
await db.close();
});
test("queries database", async t => {
const result = await db.query("SELECT 1");
assert.ok(result);
});
Example 7: Hooks with beforeEach and afterEach
Before:
import test from "ava";
test.beforeEach(t => {
t.context.store = createStore();
});
test.afterEach(t => {
t.context.store.destroy();
});
test("saves state", t => {
t.context.store.set("key", "value");
t.is(t.context.store.get("key"), "value");
});
After:
import { test } from "node:test";
import assert from "node:assert/strict";
test.beforeEach(t => {
t.context.store = createStore();
});
test.afterEach(t => {
t.context.store.destroy();
});
test("saves state", t => {
t.context.store.set("key", "value");
assert.strictEqual(t.context.store.get("key"), "value");
});
Example 8: Skip and only
Before:
import test from "ava";
test("skipped test", t => {
t.fail("should not run");
});
test.skip("explicitly skipped", t => {
t.fail("should not run");
});
test.only("only this one", t => {
t.pass();
});
After:
import { test } from "node:test";
test("skipped test", { skip: true }, t => {
// test body
});
test("explicitly skipped", { skip: true }, t => {
// test body
});
test.only("only this one", t => {
// test body
});
Example 9: Serial tests
Before:
import test from "ava";
test.serial("first sequential", t => {
global.counter = 1;
t.is(global.counter, 1);
});
test.serial("second sequential", t => {
t.is(global.counter, 1);
global.counter = 2;
});
After:
import { test } from "node:test";
import assert from "node:assert/strict";
test("first sequential", t => {
global.counter = 1;
assert.strictEqual(global.counter, 1);
});
test("second sequential", t => {
assert.strictEqual(global.counter, 1);
global.counter = 2;
});
// Note: To run these serially, use: node --test --test-concurrency=1
Example 10: Truthy and falsy assertions
Before:
import test from "ava";
test("truthiness checks", t => {
t.truthy(1);
t.falsy(0);
t.is(true, true);
});
After:
import { test } from "node:test";
import assert from "node:assert/strict";
test("truthiness checks", t => {
assert.ok(1);
assert.ok(!0);
assert.strictEqual(true, true);
});
Example 11: package.json script migration
Before:
{
"scripts": {
"test": "ava",
"test:watch": "ava --watch",
"test:serial": "ava --serial",
"coverage": "nyc ava"
},
"ava": {
"files": ["test/**/*.js"],
"timeout": "10s",
"concurrency": 4
}
}
After:
{
"scripts": {
"test": "node --test",
"test:watch": "nodemon --exec 'node --test'",
"test:serial": "node --test --test-concurrency=1",
"coverage": "c8 node --test"
}
}
Caveats
- Ava's macro functions have no direct equivalent and require manual refactoring or parameterization patterns.
- Ava-specific reporters, watch mode, and snapshot helpers are not provided by
node:test; use external tools (nodemon, tap/junit reporters, snapshot libraries). - Ava plugins and custom assertion methods may require manual rewrites or third-party assertion libraries.
- Ava's
t.contextworks similarly innode:test, but it is not automatically isolated per test—ensure cleanup intest.afterEach()to avoid test pollution.
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
No implementation or test paths are named in the issue. Start by locating the codemod entry point and its existing test suite, then review package.json and the listed Ava/node:test CLI mappings; done means supported imports, assertions, hooks, flags, and async error cases are covered, with macros, snapshots, plugins, and config migration explicitly scoped.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js, typescript
- Domain
- developer-experience, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 28/100