This document explains how to migrate old section-tests suites that use the legacy section(...) API into the new atomic test(...) model.
The target reader is an AI agent doing large-scale migration work. The guidance is explicit and pattern-based so it can be applied consistently across many files.
Share code, not runtime state.
The new model assumes:
- each test is independent
- each test may optionally define
setup -> run -> teardown - setup returns data for one test execution only
- reusable setup belongs in helper functions, not in shared live fixture instances
- tests are parallel by default
- timeouts abort the test context and may still attempt bounded teardown before the worker is terminated
Old:
section('users', (section) => {
section.test('create', async () => {
// ...
});
});New:
import { defineTests, suite, test } from 'section-tests';
export default defineTests(
suite(
'users',
test('create', async () => {
// ...
}),
),
);Notes:
- nested
section(...)trees become nestedsuite(...)trees - suite nesting is for organization and reporting only
- do not rely on suite order for behavior
Old:
section.test('create user', async () => {
const result = await createUser();
assert.ok(result);
});New:
test('create user', async () => {
const result = await createUser();
assert.ok(result);
});If there is no setup or teardown, use the short form.
Old:
section.setup(async () => {
await connectDb();
});
section.destroy(async () => {
await closeDb();
});New:
async function createDb() {
const db = await connectDb();
return db;
}
async function disposeDb(db) {
await closeDb(db);
}
test('example', {
async setup() {
return await createDb();
},
async run(db) {
// ...
},
async teardown(db) {
await disposeDb(db);
},
});Important:
- do not convert one global setup into one global shared runtime object unless the code is truly serial-only
- instead, extract reusable helper functions and call them per test
- write setup/run/teardown helpers so they can stop cooperatively when the provided
AbortSignalis aborted
If many old tests use the same setup flow, extract a helper:
function dbTest(name, params, run) {
return test(name, {
async setup() {
return await createUsersDb(params);
},
async run(db, context) {
await run(db, context);
},
async teardown(db) {
await disposeUsersDb(db);
},
});
}Use that helper instead of inventing runtime-sharing semantics.
Old:
000.setup.jsregisters reporter or setup999.main.jsassumes earlier files already ran
New:
- each file must export self-contained tests or suites
- no file may depend on another file having executed first
- move shared logic into imported helpers
Old:
section('workflow', (section) => {
section.test('step 1', async () => { ... });
section.test('step 2', async () => { ... });
section.test('step 3', async () => { ... });
});If later steps depend on earlier steps, do not keep them as separate parallel tests.
Rewrite as either:
- independent tests with their own setup, or
- one explicit scenario test
Example:
test('workflow scenario', {
async setup() {
return await createWorkflowState();
},
async run(state) {
await step1(state);
await step2(state);
await step3(state);
},
async teardown(state) {
await disposeWorkflowState(state);
},
});Old:
section.info('created user');
section.warn('slow path');New:
test('example', async (t) => {
t.info('created user');
t.warn('slow path');
});Helpers used from setup, run, or teardown should be cancellation-aware whenever possible.
Prefer:
test('example', {
async run(_setup, context) {
await doWork({ signal: context.signal });
},
});Avoid migration patterns that create long-running work with no way to stop, unless the test is truly exercising timeout behavior itself.
Or with setup:
test('example', {
async setup(t) {
t.info('preparing');
return await createThing();
},
async run(thing, t) {
t.success('running');
},
});Prefer parallel tests. Only keep a test serial if it truly touches a global or exclusive resource that cannot be isolated.
Use serial(...) for cases like:
- one real port that cannot be changed
- one external account with rate limits or global state
- one process-wide singleton that cannot be refactored yet
Do not mark tests serial just because old code used a shared setup tree.
For every old file:
- Remove implicit file-order assumptions.
- Convert
section(...)nesting tosuite(...)only when it helps naming/reporting. - Convert
section.test(...)totest(...). - Replace
section.setup(...)/section.destroy(...)with reusable helper functions plus per-test lifecycle hooks. - Ensure every test can run alone.
- Use
serial(...)only for true exclusivity. - Make helpers cancellation-aware if they may run for a long time.
- Preserve test names where possible so reporter output remains recognizable.
A migrated file should usually look like this:
import { defineTests, suite, test } from 'section-tests';
async function createThing() {
// ...
}
async function disposeThing(thing) {
// ...
}
export default defineTests(
suite(
'feature area',
test('does one thing', {
async setup() {
return await createThing();
},
async run(thing) {
// ...
},
async teardown(thing) {
await disposeThing(thing);
},
}),
),
);That shape is easy for agents to generate, easy for humans to review, and parallel-safe by default.