Node.js Native Test Runner
If anyone missed it, Node.js 18 includes a test runner/test definition module (node --test
and node:test
respectively).
node:test
exports a test
function and you can run the Node CLI with a --test
flag which does some basic search/matching for test files.
Full Documentation: nodejs.org/api/test.html
- Documentation for
node:test
- Documentation for
node --test
- Curated set of examples: github.com/HugoDF/node-test-runner-examples.
Table of Contents
Example Node.js Tests with Assertions
The recommended way to do assertions is to use the existing assert
module.
Combination of node:test
+ node:assert/strict
are good for testing pure code (ie. check output is as expected).
For module interception or advanced stubbing, you’ll have to look elsewhere, although I did find CallTracker the other day, which isn’t as complete as jest.fn() or sinon stubs but you can create a stub that reports on how many times it’s been called, see assert.CallTracker docs.
In short: you can write and run tests in Node.js without any npm packages.
A simple example, in a ping.mjs
file.
import test from 'node:test';
import assert from 'node:assert/strict';
const ping = () => 'pong';
test('ping', () => {
assert.equal(ping(), 'pong');
});
We can run the above using node --test ping.mjs
or node ping.mjs
which gives the following output:
node ping.mjs
(node:2191) ExperimentalWarning: The test runner is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
TAP version 13
ok 1 - ping
---
duration_ms: 0.001783726
...
1..1
# tests 1
# pass 1
# fail 0
# skipped 0
# todo 0
# duration_ms 0.91425132
Given a failure by changing assert.equal(ping(), 'pong');
to assert.equal(ping(), 'ping');
, we would get the following output:
node ping.mjs
(node:2321) ExperimentalWarning: The test runner is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
TAP version 13
not ok 1 - ping
---
duration_ms: 0.00915903
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
'pong' !== 'ping'
code: 'ERR_ASSERTION'
stack: |-
TestContext.<anonymous> (ping.mjs:5:10)
Test.runInAsyncScope (node:async_hooks:202:9)
Test.run (node:internal/test_runner/test:340:20)
Test.start (node:internal/test_runner/test:292:17)
Test.test (node:internal/test_runner/harness:126:18)
file://ping.mjs:5:1
ModuleJob.run (node:internal/modules/esm/module_job:198:25)
async Promise.all (index 0)
async ESMLoader.import (node:internal/modules/esm/loader:409:24)
async loadESM (node:internal/process/esm_loader:85:5)
...
1..1
# tests 1
# pass 0
# fail 1
# skipped 0
# todo 0
# duration_ms 0.684553003
See more examples at github.com/HugoDF/microbundle-ts-pkg/tree/master/test
Mocks, stubs and spies in node:test
The example for this section is available at node-test-runner-examples/src/02.01-simple-mock-assertions.test.js.
Spying and stubbing can be done natively with node:test
’s “mocking” functionality.
We can create a mock using mock.fn
and assert on its calls using mockFn.mock.callCount()
and mockFn.mock.calls[]
.
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('Simple mock assertions', (t) => {
const mockFn = t.mock.fn();
mockFn('call-arg-1', 'call-arg-2');
assert.deepEqual(mockFn.mock.calls[0].arguments, [
'call-arg-1',
'call-arg-2',
]);
assert.equal(mockFn.mock.callCount(), 1);
mockFn('call-arg-3', 'call-arg-4');
assert.equal(mockFn.mock.callCount(), 2);
});
Running the above test file at the command line yields the following output:
node src/02.01-simple-mock-assertions.test.js
✔ Simple mock assertions (1.289042ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5.653416
A failure of callCount() looks as follows:
node src/02.01-simple-mock-assertions.test.js
✖ Simple mock assertions (2.051875ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
1 !== 2
at TestContext.<anonymous> (file:///Users/hugo/Documents/projects/node-test-runner-examples/src/02.01-simple-mock-assertions.test.js:15:9)
at Test.runInAsyncScope (node:async_hooks:206:9)
at Test.run (node:internal/test_runner/test:631:25)
at Test.start (node:internal/test_runner/test:542:17)
at startSubtest (node:internal/test_runner/harness:216:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 1,
expected: 2,
operator: 'strictEqual'
}
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 7.69825
And a failure of deepEqual(...arguments, [...])
looks as follows
node src/02.01-simple-mock-assertions.test.js
✖ Simple mock assertions (2.699792ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
[
'call-arg-1',
+ 'call-arg-2'
- 'call-arg-1'
]
at TestContext.<anonymous> (file:///Users/hugo/Documents/projects/node-test-runner-examples/src/02.01-simple-mock-assertions.test.js:8:9)
at Test.runInAsyncScope (node:async_hooks:206:9)
at Test.run (node:internal/test_runner/test:631:25)
at Test.start (node:internal/test_runner/test:542:17)
at startSubtest (node:internal/test_runner/harness:216:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [Array],
expected: [Array],
operator: 'deepStrictEqual'
}
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 7.854208
More mocking examples are available at github.com/HugoDF/node-test-runner-examples.
Recommended module interception and snapshot assertion for node:test
For more advanced functionality to augment your tests:
- module interception can be done using proxyquire or esmock.
- a standalone snapshot solution is snapshot-assertion
Notes on node:test
TAP output
Back to the Node.js test runner, it outputs in TAP, which is the “test anything protocol”, here’s the specification: testanything.org/tap-specification.html.
That means you can take your output and pipe it into existing formatters and there was already node-tap as a userland runner implementation.
Closing remarks
Final couple of things, which are a given for most modern Node.js test runners:
- it supports all the usual test function formats ie. Promise rejection, error throwing,
done
callback not invoked are all reported as failures - there’s “skip/only” and “sub-tests/test suite” concepts
Node.js “test” files that use node:test
can be run directly using node file.js
for example. node --test <more-options>
can be used for running multiple files at once, see “test runner execution model”.
For a curated set of examples with node:test
, see github.com/HugoDF/node-test-runner-examples.
Find more node:test
posts on Code with Hugo by going to the node:test
tag page.
Photo by DiEGO MüLLER on Unsplash
Get The Jest Handbook (100 pages)
Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.
orJoin 1000s of developers learning about Enterprise-grade Node.js & JavaScript