Assertions and utilities that make
node:testbetter
The built-in node:assert module is intentionally minimal. This package fills the gaps with the assertions you actually miss from AVA and Jest, like a throws that returns the error and asymmetric matchers.
Every export is a standalone function that throws an AssertionError on failure, so it works in node:test, Vitest, or even a bare script. Nothing here patches or depends on a specific test runner.
npm install --save-dev test-extrasimport test from 'node:test';
import assert from 'node:assert/strict';
import {throwsAsync, matches, any, objectContaining} from 'test-extras';
test('rejects with a useful error', async () => {
const error = await throwsAsync(fetchUser('missing'), {instanceOf: TypeError});
// Unlike `assert.rejects()`, you get the error back to assert on.
assert.equal(error.code, 'ERR_NOT_FOUND');
});
test('response has the expected shape', async () => {
const response = await fetchUser('1');
matches(response, objectContaining({
id: any(String),
createdAt: any(Date),
name: 'Unicorn',
}));
});Assert that function throws, and return the thrown error so you can make further assertions on it. Unlike assert.throws(), the error is returned. Fails if nothing is thrown.
Assert that a promise rejects (or an async function throws), and return the rejection reason. Unlike assert.rejects(), the reason is returned. Fails if the promise resolves.
Assert that function does not throw, and return its return value.
Assert that a promise resolves (or an async function does not throw), and return the resolved value.
Type: object | Function | RegExp
Optional expectations checked against the thrown error. All provided fields must match.
import {throws} from 'test-extras';
const error = throws(() => JSON.parse('{'), {
instanceOf: SyntaxError, // The error must be an instance of this.
message: /JSON/, // A `RegExp` is tested; a string must match exactly.
code: 'ERR_X', // The `error.code` must equal this.
name: 'SyntaxError', // The `error.name` must equal this.
});As a shorthand, a constructor is treated as {instanceOf} and a RegExp as {message}, matching assert.throws():
throws(() => JSON.parse('{'), SyntaxError);
throws(() => JSON.parse('{'), /JSON/);Asymmetric matchers match by shape or type instead of exact value. Use them anywhere inside a matches() pattern, including nested.
Note
Matchers only work with matches(), not with assert.deepStrictEqual(). Node's deep-equality has no hook to run custom matchers (nodejs/node#55319), which is exactly why matches() exists.
Assert that actual matches pattern. Plain objects and arrays are matched structurally and require an exact key set or length, with matchers allowed at any depth. Other values (dates, maps, class instances, primitives) are compared with deep strict equality. Use objectContaining() for partial matching.
Match a value by type. The primitive wrappers (String, Number, Boolean, BigInt, Symbol) match both the primitive and its boxed form, Object matches any non-null object, and any other constructor (including Function) is checked with instanceof.
import {matches, any} from 'test-extras';
matches(42, any(Number));
matches(new Date(), any(Date));Match any value except null and undefined.
Match an object that contains at least the given properties, ignoring any extra ones. Each property value is matched as in matches(), so a nested plain object must match exactly. Nest another objectContaining() to ignore extra properties deeper down.
import {matches, objectContaining, any} from 'test-extras';
matches({id: 1, name: 'Unicorn', extra: true}, objectContaining({id: any(Number)}));Match an array that contains at least the given elements, in any order. Each element is matched as in matches(), so the subset can itself contain matchers.
import {matches, arrayContaining, any} from 'test-extras';
matches([1, 2, 3], arrayContaining([3, 1]));
matches([{id: 1}, {id: 2}], arrayContaining([{id: any(Number)}]));Match a string that includes the given substring.
import {matches, stringContaining} from 'test-extras';
matches('hello world', stringContaining('world'));Match a string against a regular expression. A string pattern is treated as a regex source. Unlike assert.match(), this works nested inside a pattern.
import {matches, stringMatching} from 'test-extras';
matches({id: 'user_42'}, {id: stringMatching(/^user_\d+$/)});Assert that actual is within delta of expected (default 1e-7). Useful for floating-point comparisons. Also works with bigints, in which case actual and expected must both be bigints.
import {closeTo} from 'test-extras';
closeTo(0.1 + 0.2, 0.3);Assert that a string, array, or Set contains item.
import {includes} from 'test-extras';
includes([1, 2, 3], 2);
includes('unicorn', 'corn');Assert that a string, array, or Set does not contain item. The inverse of includes().
import {excludes} from 'test-extras';
excludes([1, 2, 3], 4);Assert that value is one of values.
import {oneOf} from 'test-extras';
oneOf('active', ['active', 'pending', 'closed']);Assert that two arrays contain the same elements, regardless of order. Compared with deep equality, and duplicate-sensitive (a multiset). Unlike arrayContaining(), this requires an exact match, not a subset; unlike assert.deepStrictEqual(), order is ignored.
import {sameMembers} from 'test-extras';
sameMembers([3, 1, 2], [1, 2, 3]);Assert that value is an instance of constructor. In TypeScript, this narrows the type of value.
import {instanceOf} from 'test-extras';
instanceOf(error, TypeError);Assert that value has the given length.
Assert that value is (not) empty: a string/array with no items, a Set/Map with no entries, or an object with no own enumerable keys. Unlike hasLength(), this also covers Set, Map, and objects.
import {isEmpty, isNotEmpty} from 'test-extras';
isEmpty(new Map());
isNotEmpty([1, 2, 3]);Assert that value is falsy. The inverse of assert.ok().
Assert a relation between two numbers or bigints.
Assert that value is within start and end, inclusive of both bounds.
import {inRange} from 'test-extras';
inRange(5, 1, 10);Assert that string starts/ends with the given substring. More readable than an anchored assert.match(string, /^prefix/).
import {startsWith, endsWith} from 'test-extras';
startsWith('unicorn', 'uni');
endsWith('unicorn', 'corn');Run callback (sync or async) while capturing everything written to process.stdout and process.stderr (including console.log and console.error), then restore them and return the captured text. Useful for testing CLI output.
import {captureOutput} from 'test-extras';
import assert from 'node:assert/strict';
const {stdout, stderr} = await captureOutput(() => {
console.log('Hello');
});
assert.equal(stdout, 'Hello\n');Note
This patches the process-wide streams, so do not rely on it while other code writes concurrently.
Run callback (sync or async) with process.env temporarily overridden by variables, then restore the original environment afterward (even if callback throws), and return the callback's value. A variable set to undefined is unset for the duration of the callback.
import {withEnvironment} from 'test-extras';
const config = await withEnvironment({NODE_ENV: 'production', DEBUG: undefined}, () => loadConfig());Repeatedly call condition (sync or async) until it returns a truthy value, then resolve with that value. If condition throws, it retries (useful while a resource is not ready yet) and only surfaces the error if it times out; if condition keeps returning a falsy value until the timeout, it rejects with a TimeoutError. node:test has no built-in way to wait for an asynchronous condition.
import {waitFor} from 'test-extras';
const row = await waitFor(() => database.find(42));
await waitFor(() => server.isReady, {timeout: 5000});Type: object
Type: number
Default: 50
Milliseconds to wait between attempts.
Type: number
Default: 1000
Milliseconds to wait before giving up and rejecting.
Type: AbortSignal
Abort the wait early. When the signal aborts, the returned promise rejects with the signal's reason.
Wait for emitter to emit eventName, then resolve with the first value passed to it. Rejects with a TimeoutError if the event does not fire within the timeout. Like events.once(), it also rejects if the emitter emits an error event while waiting. Unlike waitFor, which polls a condition, this subscribes to a single event; unlike events.once(), it has a timeout.
import {waitForEvent} from 'test-extras';
server.listen(0);
await waitForEvent(server, 'listening');Type: object
Type: number
Default: 1000
Milliseconds to wait before giving up and rejecting.
Type: AbortSignal
Abort the wait early. When the signal aborts, the returned promise rejects with the signal's reason.