nodejs / nodejs/userland-migrations

spec: `ava-to-node-test`

Open
#319 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

good first issue human help wanted let's do it
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 ava imports with node:test and node:assert/strict equivalents.
  • The codemod should convert test() blocks to test() while preserving names, options, and async handling.
  • The codemod should map ava assertion helpers (t.is, t.deepEqual, t.throws, t.truthy, etc.) to assert/strict equivalents.
  • The codemod should migrate hook APIs (test.before, test.after, test.beforeEach, test.afterEach) to their node:test counterparts.
  • The codemod should transform ava-specific flags (serial, only, skip) to their node:test equivalents.
  • 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:test ships with Node.js (stable since v20) and uses the node:assert/strict API for assertions by default.
  • Test discovery is handled by node --test instead of the ava CLI; suite-level configuration (reporters, concurrency, timeout) can be updated in a follow-up workflow step that edits package.json or config files.
  • The t object from node:test exposes similar helpers (test, plan, after, diagnostic) but not ava-specific features like macros or snapshots.
  • test.serial maps to node --test --test-concurrency=1 or per-file serial markers; there is no per-test serial equivalent in node:test.
  • Ava's t.throwsAsync() should be converted to assert.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 ... with node --test plus equivalent flags: --test-only (ava --match with exact match), --test-name-pattern (ava --match), --test-timeout (ava --timeout), and --test-concurrency (ava --concurrency).
  • Translate ava's file patterns (files config) to positional arguments or file globs passed to node --test.
  • Coverage options (.nyc_config or c8 setup) require external tools; use c8 node --test as a separate workflow step.
  • Ava's --watch mode, custom reporters, and tap output are not built into node:test; use external tools (nodemon, custom reporters) for equivalent functionality.
  • Remove .avarc.json, ava.config.js, or ava config 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.context works similarly in node:test, but it is not automatically isolated per test—ensure cleanup in test.afterEach() to avoid test pollution.

Refs

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.