| name | node-testing |
| description | Write and run tests using Node.js 25's built-in test runner (node:test). Zero dependencies — includes describe/it, mocking, coverage, expectFailure, rerun-failures, and TypeScript type stripping. Use when: writing unit/integration tests, running test suites, checking coverage, mocking functions, or testing .ts files directly. |
| metadata | {"openclaw":{"emoji":"🧪","requires":{"bins":["node"]},"compat":"Node.js >= 25.x (24.x partially supported; expectFailure requires 25.5+)"}} |
Node.js 25 Built-In Test Runner
Zero-dependency testing with node:test. No Jest, no Mocha, no Vitest — everything is built in.
When to Use
✅ USE this skill when:
- Writing unit tests or integration tests for Node.js projects
- Running test suites for CLI tools, APIs, libraries
- Checking code coverage on a project
- Mocking functions, methods, or timers in tests
- Testing TypeScript files directly (type stripping)
- User says: "run the tests", "write tests for X", "test coverage", "mock something"
❌ DON'T use this skill when:
- Browser/frontend testing (use Playwright, Vitest browser mode)
- React component testing (use Vitest + Testing Library)
- Projects locked to Jest/Vitest (respect existing config)
Quick Start
node --test
node --test src/math.test.js
node --test --experimental-test-coverage
Core APIs
describe / it (BDD-style)
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert';
describe('Calculator', () => {
let calc;
beforeEach(() => {
calc = { add: (a, b) => a + b };
});
it('should add two numbers', () => {
assert.strictEqual(calc.add(2, 3), 5);
});
it('should handle negatives', () => {
assert.strictEqual(calc.add(-1, 1), 0);
});
describe('nested suite', () => {
it('works inside describe', () => {
assert.ok(true);
});
});
});
Aliases: describe = suite, it = test. Use whichever you prefer.
test() (functional style)
import test from 'node:test';
import assert from 'node:assert';
test('synchronous', () => {
assert.strictEqual(1, 1);
});
test('async', async () => {
const result = await fetch('https://example.com');
assert.ok(result.ok);
});
test('callback', (t, done) => {
setTimeout(done, 100);
});
Subtests
test('parent', async (t) => {
await t.test('child 1', () => { });
await t.test('child 2', () => { });
});
Built-In Mocking
No jest.fn() needed. Mocking is built into node:test.
Mock Functions (Spy)
import { mock, test } from 'node:test';
import assert from 'node:assert';
test('spy on a function', () => {
const fn = mock.fn((a, b) => a + b);
assert.strictEqual(fn(3, 4), 7);
assert.strictEqual(fn.mock.callCount(), 1);
const call = fn.mock.calls[0];
assert.deepStrictEqual(call.arguments, [3, 4]);
assert.strictEqual(call.result, 7);
mock.reset();
});
Mock Methods (auto-restore via test context)
test('mock object method', (t) => {
const obj = {
value: 5,
add(a) { return this.value + a; }
};
t.mock.method(obj, 'add');
obj.add(3);
assert.strictEqual(obj.add.mock.callCount(), 1);
assert.deepStrictEqual(obj.add.mock.calls[0].arguments, [3]);
});
Mock Timers
import { mock, test } from 'node:test';
import assert from 'node:assert';
test('mock setTimeout', () => {
const fn = mock.fn();
mock.timers.enable({ apis: ['setTimeout'] });
setTimeout(fn, 9999);
assert.strictEqual(fn.mock.callCount(), 0);
mock.timers.tick(9999);
assert.strictEqual(fn.mock.callCount(), 1);
mock.timers.reset();
mock.reset();
});
Supported timer APIs: setTimeout, setInterval, Date.now(), Date constructor, setImmediate.
Mock Modules (getters/setters)
test('mock getter', (t) => {
const obj = { get val() { return 'real'; } };
t.mock.getter(obj, 'val', () => 'mocked');
assert.strictEqual(obj.val, 'mocked');
});
Test Options
Skipping Tests
it('skipped', { skip: true }, () => { });
it('skipped with reason', { skip: 'bug #123' }, () => {});
it('also skipped', (t) => {
t.skip();
t.skip('reason');
});
describe.skip('skipped suite', () => { });
TODO Tests (expected failures, v25.5+)
TODO tests execute but don't fail the suite. Use for pending work.
it('not yet implemented', { todo: true }, () => {
throw new Error('this does NOT fail the test');
});
it('with message', { todo: 'needs refactoring' }, () => {});
expectFailure — Mark Known Bugs (v25.5+)
🔄 Reversed logic: The test must throw to pass. If it doesn't throw (i.e., it runs clean), it fails.
Perfect for tracking known bugs: when the bug is fixed, the test starts failing — alerting you to remove the flag.
it.expectFailure('known bug: fails intermittently', () => {
assert.strictEqual(buggyFunction(), expected);
});
it('known bug', { expectFailure: true }, () => {
assert.strictEqual(buggyFunction(), expected);
});
it('known bug', { expectFailure: 'feature not implemented' }, () => {
assert.strictEqual(buggyFunction(), expected);
});
it('fails with specific error', { expectFailure: /timeout/i }, () => {
throw new Error('Connection timeout');
});
it('error code match', { expectFailure: { code: 'ERR_EXPECTED' } }, () => {
const err = new Error('boom');
err.code = 'ERR_EXPECTED';
throw err;
});
it('specific failure', {
expectFailure: { label: 'known bug', match: /error msg/ }
}, () => {
throw new Error('error msg here');
});
Priority: skip > todo > expectFailure. If a test has both skip and expectFailure, it's skipped.
Only Tests (focused runs)
node --test --test-only
it.only('only this runs', () => { });
describe.only('only this suite', () => { });
Filtering by Name Pattern
node --test --test-name-pattern="should add"
node --test --test-name-pattern="/should add/i"
node --test --test-skip-pattern="slow"
Code Coverage
node --test --experimental-test-coverage
node --test --experimental-test-coverage \
--test-reporter=lcov \
--test-reporter-destination=coverage.info
node --test --experimental-test-coverage \
--test-coverage-include="**/*.js"
node --test --experimental-test-coverage \
--test-coverage-exclude="**/migrations/**"
Inline Coverage Control
if (neverReached) { console.log('nope'); }
if (neverReached) {
console.log('line 1');
console.log('line 2');
}
--test-rerun-failures — Only Rerun Failed Tests
Instead of re-running the entire suite, persist state and only re-run what failed:
node --test --test-rerun-failures ./test-state.json
node --test --test-rerun-failures ./test-state.json
node --test --test-rerun-failures ./test-state.json
The state file is JSON tracking which tests passed on which attempt. Each subsequent run skips passing tests and only re-executes failures. Great for CI flaky-test retry loops.
TypeScript — Type Stripping (Zero Config)
Node.js 25 has built-in type stripping. No ts-node, no tsx, no transpile step.
node --test src/utils.test.ts
node --test --no-strip-types src/utils.test.ts
Auto-discovery of .ts test files (when type stripping is enabled):
*.test.{cts,mts,ts}
*-test.{cts,mts,ts}
*_test.{cts,mts,ts}
test-*.{cts,mts,ts}
test.{cts,mts,ts}
test/**/*.{cts,mts,ts}
Limitations: Type stripping removes types but does NOT check them. Run tsc --noEmit separately for type checking.
import { describe, it } from 'node:test';
import assert from 'node:assert';
function add(a: number, b: number): number {
return a + b;
}
describe('add', () => {
it('sums correctly', () => {
assert.strictEqual(add(2, 3), 5);
});
});
Watch Mode
node --test --watch
Automatically re-runs tests when files change. Good for TDD workflow.
Global Setup / Teardown (v24+)
export async function globalSetup() {
console.log('Starting DB...');
}
export async function globalTeardown() {
console.log('Stopping DB...');
}
node --test --test-global-setup=./setup.js
Reporters
node --test --test-reporter=spec
node --test --test-reporter=tap
node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info
node --test --test-reporter=dot
Key Flags Reference
| Flag | Description |
|---|
--test | Enable test runner, auto-discover test files |
--test-concurrency=N | Max parallel child processes (default: CPU count) |
--test-name-pattern=REGEX | Run only tests matching pattern |
--test-skip-pattern=REGEX | Skip tests matching pattern |
--test-only | Only run tests with { only: true } |
--test-rerun-failures=FILE | Persist state, only rerun failed tests |
--test-reporter=TYPE | Output format: spec, tap, dot, lcov |
--test-reporter-destination=FILE | Reporter output destination |
--test-global-setup=FILE | Global setup/teardown module |
--experimental-test-coverage | Enable code coverage collection |
--test-coverage-include=GLOB | Coverage include pattern |
--test-coverage-exclude=GLOB | Coverage exclude pattern |
--watch | Re-run tests on file changes |
--test-force-exit | Force exit after tests complete |
--experimental-strip-types | Enable TS type stripping (default in Node 25) |
--no-strip-types | Disable type stripping |