Node.js includes a stable, built-in test runner available through the node:test module. It provides a modern testing experience without requiring external dependencies.
The test runner has been stable since Node.js v20.0.0.
import test from 'node:test';import assert from 'node:assert';test('synchronous passing test', (t) => { // This test passes because it does not throw an exception assert.strictEqual(1, 1);});test('asynchronous passing test', async (t) => { // This test passes because the Promise fulfills assert.strictEqual(1, 1);});
# Run a single test filenode --test path/to/test.js# Run multiple test filesnode --test test/unit/*.test.js# Run with glob patternnode --test "test/**/*.test.js"
// Assert function throwsassert.throws( () => { throw new Error('expected error'); }, Error);// Assert async function rejectsawait assert.rejects( async () => { throw new Error('expected error'); }, Error);// Assert function doesn't throwassert.doesNotThrow(() => { // code that should not throw});
import { describe, it, before, after } from 'node:test';describe('Suite setup', () => { before(async () => { // Runs once before all tests in this suite await setupTestEnvironment(); }); after(async () => { // Runs once after all tests in this suite await teardownTestEnvironment(); }); it('test 1', () => { /* ... */ }); it('test 2', () => { /* ... */ });});
// Using skip optiontest('skip this test', { skip: true }, (t) => { // This code is never executed});// With messagetest('skip with message', { skip: 'Not implemented yet' }, (t) => { // This code is never executed});// Using skip() methodtest('conditional skip', (t) => { if (process.platform === 'win32') { t.skip('Not supported on Windows'); return; } // Test continues on other platforms});
test('todo test', { todo: true }, (t) => { // This executes but doesn't fail the test suite throw new Error('this does not fail the test');});test('todo with message', { todo: 'Feature not implemented' }, (t) => { // Test runs but is marked as TODO});it.todo('implement this feature');
TODO tests are executed but not treated as failures, and don’t affect the exit code.
test.only('this test runs', () => { assert.ok(true);});test('this test is skipped', () => { assert.ok(true);});it.only('only this test runs', () => { assert.ok(true);});
# First run - creates state filenode --test --test-rerun-failures=./test-state.json# Subsequent runs - only failed testsnode --test --test-rerun-failures=./test-state.json