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.EXT
test-NAME.EXT
NAME.test.EXT
|NAME-test.EXT
|NAME_test.EXT
- Any
NAME.EXT
under atest
directory, 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.
Beautiful!