Flash cards
Review the key moves
What is the main idea behind Node.js Test Runner?
Lesson checks
Practice each idea before moving on
Short Mimo-style checks built from this lesson's code, terms, and sequence.
Which statement best captures the main point of this lesson?
Complete the missing token from the example code.
// ___ the test modulePut the learning moves in the order that makes the concept easiest to apply.
Introduction to Node.js Test Runner
The built-in node:test module provides a lightweight, no-dependency framework for writing and running JavaScript tests directly in Node.js.
Introduced as a stable API in Node.js 20, it's designed to be a fast, modern alternative to external testing frameworks.
Note
The Node.js Test Runner is stable as of Node.js v20.
Some advanced features may be experimental in earlier versions.
Key Features
- Zero Configuration: Works out of the box with no setup
- Dual Module Support: Native ESM and CommonJS compatibility
- Parallel Execution: Tests run concurrently by default
- Test Isolation: Each test runs in its own context
- Async Support: First-class async/await handling
- Test Hooks: Before/After hooks for setup/teardown
- Mocking: Built-in test doubles and spies
- Code Coverage: Integration with Node.js coverage tools
Writing Your First Test
Let's create and run a basic test using the Node.js Test Runner.
You'll need Node.js 16.17.0 or later installed.
// Load the test module
const test = require('node:test');
// Use strict assertion mode for better error messages
const assert = require('node:assert/strict');
// Simple synchronous test test('basic arithmetic', (t) => {
// Assert that 1 + 1 equals 2
assert.equal(1 + 1, 2, '1 + 1 should equal 2');
// Deep equality check for objects/arrays
assert.deepEqual(
{ a: 1, b: { c: 2 } },
{ a: 1, b: { c: 2 } }
);
});
// Asynchronous test with async/await
test('async test', async (t) => {
const result = await Promise.resolve('async result');
assert.strictEqual(result, 'async result');
});# Run all test files in the test directory
node --test
# Run a specific test file
node --test test/example.test.js
# Run with coverage reporting
NODE_V8_COVERAGE=coverage node --testTest Structure and Organization
For larger projects, organize your tests in a structured way:
project/
âââ src/
â âââ math.js
â âââ utils.js
âââ test/
âââ unit/
â âââ math.test.js
â âââ utils.test.js
âââ integration/
âââ api.test.jsTest Hooks
Use hooks to set up and clean up test environments:
const { test, describe, before, after, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
describe('Test Suite with Hooks', (t) => {
let testData = [];
// Runs once before all tests
before(() => {
console.log('Running before all tests');
testData = [1, 2, 3];
});
// Runs before each test
beforeEach((t) => {
console.log('Running before each test');
});
test('array length', () => {
assert.strictEqual(testData.length, 3);
});
// Runs after each test
afterEach(() => {
console.log('Running after each test');
});
// Runs once after all tests
after(() => {
console.log('Running after all tests');
testData = [];
});
});// simple-test.js
const test = require('node:test');
const assert = require('node:assert/strict');
test('basic test', () => {
assert.equal(1 + 1, 2);
});Running Tests
Run tests using the --test flag
node --test simple-test.jsYou can also run all test files in a directory:
node --testThis will run all files with names matching these patterns:
- **/*.test.js
- **/*.spec.js
- **/test-*.js
- **/test/*.js
Asynchronous Tests
For asynchronous code, use an async test function:
import test from 'node:test';
import assert from 'node:assert/strict';
// Using async/await
test('async test', async () => {
// Simulate async operation
const result = await Promise.resolve(42);
assert.equal(result, 42);
});
// Using callbacks with done (older style)
test('callback test', (t, done) => {
setTimeout(() => {
assert.equal(1 + 1, 2);
done();
}, 100);
});Subtests (Nested Tests)
You can organize related tests using subtests:
import test from 'node:test';
import assert from 'node:assert/strict';
test('math operations', async (t) => {
await t.test('addition', () => {
assert.equal(1 + 1, 2);
});
await t.test('multiplication', () => {
assert.equal(2 * 3, 6);
});
await t.test('division', () => {
assert.equal(10 / 2, 5);
});
});Setup and Teardown (Test Fixtures)
For tests that need setup and teardown, use the t.before() and t.after() hooks:
import test from 'node:test';
import assert from 'node:assert/strict';
test('using test fixtures', async (t) => {
// Setup - runs before the test
t.before(() => {
console.log('Setting up test resources');
// Example: Create test database, mock services, etc.
});
// Actual test
await t.test('my test with fixtures', () => {
assert.equal(1 + 1, 2);
});
// Teardown - runs after the test
t.after(() => {
console.log('Cleaning up test resources');
// Example: Delete test database, restore mocks, etc.
});
});Skipping and Todo Tests
You can mark tests to be skipped or as todos:
import test from 'node:test';
// Skip this test
test('skipped test', { skip: true }, () => {
// This won't run
});
// Skip with a reason
test('skipped with reason', { skip: 'working on this later' }, () => {
// This won't run
});
// Mark as TODO
test('todo test', { todo: true }, () => {
// This won't run, but will be reported as TODO
});
// Conditional skip
test('conditional skip', { skip: process.platform === 'win32' }, () => {
// This will be skipped on Windows
});Assertions
The Node.js Test Runner works with the built-in assert module. For stricter equality checks, use assert/strict .
import assert from 'node:assert/strict';
// Equality checks
assert.equal(1, 1); // Loose equality (==)
assert.strictEqual(1, 1); // Strict equality (===)
assert.deepEqual({a: 1}, {a: 1}); // Deep equality for objects
assert.deepStrictEqual({a: 1}, {a: 1}); // Strict deep equality
// Truthiness checks
assert.ok(true); // Checks if value is truthy
assert.ok(1); // Also truthy
// Comparing values
assert.notEqual(1, 2); // Check inequality
assert.notStrictEqual(1, '1'); // Check strict inequality
// Throwing errors
assert.throws(() => { throw new Error('Boom!'); }); // Check if function throws
assert.doesNotThrow(() => { return 42; }); // Check if no error thrown
// Async assertions
await assert.rejects( // Check if Promise rejects
async () => { throw new Error('Async boom!'); }
);Working with Mocks
The Node.js Test Runner doesn't include built-in mocking, but you can:
- Use dependency injection to provide test doubles
- Create simple mock functions and objects
- Integrate with third-party mocking libraries if needed
import test from 'node:test';
import assert from 'node:assert/strict';
// Function we want to test
function processUser(user, logger) {
if (!user.name) {
logger.error('User has no name');
return false;
}
logger.info(`Processing user: ${user.name}`);
return true;
}
// Test with a mock logger
test('processUser logs correctly', () => {
// Create a mock logger
const mockCalls = [];
const mockLogger = {
error: (msg) => mockCalls.push(['error', msg]),
info: (msg) => mockCalls.push(['info', msg])
};
// Test with valid user
const validResult = processUser({name: 'Alice'}, mockLogger);
assert.strictEqual(validResult, true);
assert.deepStrictEqual(mockCalls[0], ['info', 'Processing user: Alice']);
// Reset mock calls
mockCalls.length = 0;
// Test with invalid user
const invalidResult = processUser({}, mockLogger);
assert.strictEqual(invalidResult, false);
assert.deepStrictEqual(mockCalls[0], ['error', 'User has no name']);
});Testing a Utility Function
// utils.js
exports.formatPrice = function(price) {
if (typeof price !== 'number' || isNaN(price)) {
throw new Error('Price must be a valid number');
}
return `$${price.toFixed(2)}`;
};
// utils.test.js
const test = require('node:test');
const assert = require('node:assert/strict');
const { formatPrice } = require('./utils');
// Test cases
test('formatPrice formats numbers as currency strings', (t) => {
assert.equal(formatPrice(10), '$10.00');
assert.equal(formatPrice(10.5), '$10.50');
assert.equal(formatPrice(0), '$0.00');
});
// Test for error
test('formatPrice throws error for invalid inputs', (t) => {
assert.throws(() => formatPrice('not a number'), {
message: 'Price must be a valid number'
});
assert.throws(() => formatPrice(NaN));
assert.throws(() => formatPrice());
});Testing an API Endpoint
// userService.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/users/:id', (req, res) => {
const userId = parseInt(req.params.id);
// Simplified - in real app would fetch from database
if (userId === 1) {
res.json({ id: 1, name: 'John Doe', email: 'john@example.com' });
} else {
res.status(404).json({ error: 'User not found' });
}
});
module.exports = app;
// userService.test.js
const test = require('node:test');
const assert = require('node:assert/strict');
const http = require('node:http');
const app = require('./userService');
test('GET /users/:id returns correct user', async (t) => {
// Start the server
const server = http.createServer(app);
await new Promise(resolve => server.listen(0, resolve));
const port = server.address().port;
try {
// Make request to our API
const response = await fetch(`http://localhost:${port}/users/1`);
assert.equal(response.status, 200, 'Status should be 200');
const user = await response.json();
assert.deepStrictEqual(user, {
id: 1,
name: 'John Doe',
email: 'john@example.com'
});
// Test not found case
const notFoundResponse = await fetch(`http://localhost:${port}/users/999`);
assert.equal(notFoundResponse.status, 404, 'Status should be 404');
} finally {
// Clean up - close the server
await new Promise(resolve => server.close(resolve));
}
});Custom Reporters
You can specify different output formats for test results:
node --test --test-reporter=specAvailable reporters include
- spec - Detailed hierarchical view
- dot - Minimal dots output
- tap - Test Anything Protocol format
- junit - JUnit XML format
Filtering Tests
You can filter which tests to run using patterns:
node --test --test-name-pattern="user"This runs only tests with "user" in their name.
Watch Mode
For development, you can run tests in watch mode to automatically rerun when files change:
node --test --watchComparison with Other Testing Frameworks
| Feature | Node.js Test Runner | Jest | Mocha | Vitest |
|---|---|---|---|---|
| Built-in | â Yes (Node.js 16.17.0+) | â No | â No | â No |
| Zero Config | â Yes | â Yes | â Needs setup | â Yes |
| Test Runner | Node.js built-in | Jest | Mocha | Vite |
| Assertion Library | node:assert | Jest Expect | Chai/Sinon | Jest-compatible |
| Parallel Tests | â Yes | â Yes | â With --parallel | â Yes |
| Code Coverage | â With NODE_V8_COVERAGE | â Built-in | â Needs nyc/istanbul | â Built-in |
| Mocking | â Basic | â Advanced | â Needs Sinon | â Advanced |
| Watch Mode | â Yes (--watch) | â Yes | â With --watch | â Fast HMR |
| Best For | Built-in solution, simple projects | Full-featured testing | Flexible testing | Vite projects, ESM |
Note
The Node.js Test Runner is ideal for projects that want a lightweight, zero-dependency testing solution that's built into Node.js itself.
For more complex testing needs, Jest or Mocha might be better choices.