Node.js node:test Suites and Subtests Guide
The native node:test runner in Node.js provides a
robust, zero-dependency way to structure and execute test suites. This
article explains how the framework handles test suites and subtests,
demonstrating how to group test cases logically using both the
programmatic nested test syntax and the traditional BDD
(Behavior-Driven Development) describe and it
syntax.
Subtests Using Nested Test Functions
The primary way the node:test module handles hierarchy
is through nested tests. When a test function is executed, it receives a
test context object (commonly named t) as its first
argument. You can define subtests by calling the test()
method directly on this context object.
When you use this approach, parent tests will wait for all of their subtests to complete before finishing.
import test from 'node:test';
import assert from 'node:assert';
test('Database Operations Suite', async (t) => {
// Setup logic for the suite can go here
await t.test('should successfully write to the database', () => {
const status = saveToDb({ id: 1, name: 'Alice' });
assert.strictEqual(status, true);
});
await t.test('should fail when writing duplicate keys', () => {
const status = saveToDb({ id: 1, name: 'Bob' });
assert.strictEqual(status, false);
});
});
function saveToDb(user) {
// Mock database logic
return user.id !== 1 || user.name === 'Alice';
}In this structure, the parent test acts as the suite. If any subtest fails, the parent test is marked as failed.
Test Suites Using describe and it
For developers transitioning from frameworks like Mocha or Jest,
node:test offers a BDD-style syntax using
describe and it blocks. These functions are
imported from the node:test module and allow you to declare
suites and test cases cleanly.
import { describe, it } from 'node:test';
import assert from 'node:assert';
describe('User Service Suite', () => {
describe('Validation', () => {
it('should reject usernames shorter than 3 characters', () => {
assert.throws(() => validateUsername('ab'), /Too short/);
});
it('should accept valid usernames', () => {
assert.doesNotThrow(() => validateUsername('alice'));
});
});
});
function validateUsername(username) {
if (username.length < 3) {
throw new Error('Too short');
}
}The describe block creates a test suite container, and
it defines individual test cases. Just like nested
test contexts, describe blocks can be nested
inside other describe blocks to create multi-layered test
hierarchies.
Concurrency and Execution Order
By default, subtests defined within a parent test or
describe block run sequentially in the order they are
declared. This ensures predictable execution and prevents race
conditions when interacting with shared state.
If you want subtests to run concurrently to speed up execution, you
can pass the concurrency option. When
concurrency is enabled, the runner will execute multiple
subtests simultaneously.
test('Concurrent Subtests Suite', { concurrency: true }, async (t) => {
await t.test('Fast task', async () => {
assert.ok(true);
});
await t.test('Slow task', async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
assert.ok(true);
});
});Filtering Suites and Subtests
You can selectively run or skip specific suites and subtests using configuration options:
skip: Instructs the runner to ignore a test or a suite.only: Instructs the runner to run only that test/suite (requires running Node.js with the--test-onlycommand-line flag).
import { describe, it } from 'node:test';
describe('Payment Gateway', () => {
// This entire block will be skipped
describe.skip('Legacy Integration', () => {
it('should process transactions', () => {
// test code
});
});
describe('Stripe Integration', () => {
it('should process successful payments', () => {
// test code
});
});
});