Node.js v18 introduces test runner support. This currently experimental feature gives developers the benefits of a structured test harness for their code without having to install a third party test framework, like Mocha or Jest, as a dependency. Using the test runner produces TAP output.
The online reference provides the most up-to-date, authoritative reference and have plenty of good testing examples. However, there are a few points that might not be immediately obvious from the reference, so those are highlighted here.
Create an empty test file named a.js:
touch a.js
Run the following command:
node --test a.js
You should see output similar to the following:
$ node --test a.js
TAP version 13
# Subtest: /path/to/a.js
ok 1 - /path/to/a.js
---
duration_ms: 0.042418125
...
1..1
# tests 1
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 0.074304125
Now modify a.js so that the module exits with a non-zero exit code:
// Test will fail with any non-zero exit code
process.exit(1);You should see output similar to the following:
$ node --test a.js
TAP version 13
# Subtest: /path/to/a.js
not ok 1 - /path/to/a.js
---
duration_ms: 0.040703959
failureType: 'subtestsFailed'
exitCode: 1
stdout: ''
stderr: ''
error: 'test failed'
code: 'ERR_TEST_FAILURE'
...
1..1
# tests 1
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 0.0623685
You can test multiple modules explicitly. For example, with two separate test modules (one that returns a non-zero exit code), you would see output similar to this:
$ node --test a.js b.js
TAP version 13
# Subtest: /path/to/a.js
not ok 1 - /path/to/a.js
---
duration_ms: 0.040011959
failureType: 'subtestsFailed'
exitCode: 1
stdout: ''
stderr: ''
error: 'test failed'
code: 'ERR_TEST_FAILURE'
...
# Subtest: /path/to/b.js
ok 2 - /path/to/b.js
---
duration_ms: 0.038038583
...
1..2
# tests 2
# pass 1
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 0.063258125
In the previous section, tests were specified explicitly. You can read the
online reference for specifics, but generally node --test will find and
execute tests if any of the following naming patterns are used:
- Files are named any of:
test.EXTtest-NAME.EXTNAME.test.EXT|NAME-test.EXT|NAME_test.EXT
- Any
NAME.EXTunder atestdirectory, recursively.
Where EXT is one of js|cjs|mjs.
Create test/test.js:
import test from "node:test";
import {strict as assert} from "node:assert";Add a test function:
test("should always be true", () => {
assert(true);
});And test:
$ node --test
TAP version 13
# Subtest: /path/to/test/test.js
ok 1 - /path/to/test/test.js
---
duration_ms: 0.048303625
...
1..1
# tests 1
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 0.075651083
You can use the TestContext object supplied to your test callback. One
potentially useful method is TestContext.diagnostic, shown below (presumably
you would use this to provide more useful information as diagnostic output of
your test code than would be summarized as part of a custom assert error
message). The diagnostic messages appear after the stack trace for the
failed test results.
test("should be 5", t => {
t.diagnostic("***DIAGNOSTIC: about to assign val");
const val = 2 + 2;
//t.diagnostic(`***DIAGNOSTIC: val=${val}`);
assert.equal(5, val);
});Tests can be nested like this:
test("test suite", t => {
t.test("a", t => {
});
t.test("b", t => {
});
});If you're willing to give up access to the TestContext object, you can
simplify writing test suites using describe and it. You can still nest
a test under describe, if you want to:
import test, {describe, it} from "node:test";
import {strict as assert} from "node:assert";
describe("test suite", () => {
it("is always true", () => {
assert(true);
});
it("tests val", {skip: true}, () => {
const val = 2 + 2;
assert.equal(5, val);
});
it("tests val", {todo: true}, () => {
const val = 2 + 2;
assert.equal(5, val);
});
test("val is 5", t => {
t.diagnostic("***DIAGNOSTIC: about to assign val");
const val = 2 + 2;
t.diagnostic(`***DIAGNOSTIC: val=${val}`);
assert.equal(5, val);
});
});Aside from explicitly running test modules that aren't automatically run by
matching the naming patterns described previously, you can control which tests
run using the --test-only option.
$ node --test-only test/test.js
// Run this test suite
test("test suite", {only: true}, async t => {
// Only run tests with the `only` option set.
t.runOnly(true);
await t.test("this test is skipped");
await t.test("this test is also skipped");
await t.test("nested test suite will run", {only: true}, async t => {
// Only run tests with the `only` option set.
t.runOnly(true);
await t.test("this test is skipped", () => {
assert(false);
});
await t.test("will succeed", {only: true}, () => {
assert(true);
});
});
});
test("this test suite is skipped", async t => {
await t.test("this test is skipped", () => {
assert(false);
});
});Only one test in the example above will be run. See the --test-only section for more details.