In Node v18, NodeJS introduced a native test runner module. The test runner module exposes an API for creating and executing JavaScript tests, eliminating the need for external libraries like Jest for testing.
Earlier versions of NodeJS ran tests without third-party libraries using the native assert
module, but the assert
module had a significant limitation.
The assert
module is not a test running tool. You can use it to make assertions, but it would only display the test results if the test fails. Node v18 solves this with the test-runner module that displays the test results in the standard TAP (Test Anything Protocol) format.
Although the testing module is still in its first stage, it works seamlessly with the assert
module for writing and running single and multiple test suites.
In this article, you will learn how to use the NodeJS test runner module to write and execute unit tests without the help of any external library.
Prerequisites
- You must have NodeJS v18 installed on your system.
- It’s also good to have basic knowledge of the
assert
module, but this is not required.
Using the Test Runner Module
The test runner module is available under node:
scheme. The node:
scheme is a new protocol by NodeJS that distinguishes core node packages from user-created packages by prefixing node:
to the package name.
To start testing with the module, you must import the test
module and the assert
module into your test suite, like so:
const assert = require("assert"); const test = require("node:test");
The NodeJS test runner module supports a lot of features which include:
- Subtests
- Test skipping
- Callback tests, etc.
Syntax of the Test Runner Module
Here’s what a simple test written with the test runner module looks like:
const multiply = (a, b) => { return a * b; }; //Passing Test test("Multiply numbers", (t) => { assert.strictEqual(multiply(2, 2), 4); });
The test
function takes in two arguments; the first is a string that describes the test and a callback function containing the test logic. Additionally, it accepts an optional options object. The callback function takes two parameters, t
and done
(optional).
t
is the test context; it allows subtests to be created. It behaves identically to its top-level test
.
done
is used when callback functions are used in tests instead of promises. A downside of using callbacks is that the test completes without calling the callback.
To solve this problem, done
is passed in as a parameter, and the test won’t complete until the done function is called or it fails by timing out.
According to the documentation, the test is considered failing if done
receives any truthy value as its first argument.
The test is considered passing if a falsy value is passed as the first argument to done
.
The test will fail if the test receives done
and returns a promise. If the test will return a promise, there is no need for done
since its primary function is to pause execution till the callback function is executed.
If you run the test above, the output should look like this:
The image above is an example of results displayed in the TAP format.
Writing Subtests with the Test Runner Module
When writing multiple tests for a single function, it is good practice to organize them using subtests.
When writing subtests, the test
function behaves like Jest’s describe
, which describes top-level tests. You use the test
function to describe all the subtests generally. Then write the subtests with the test context, t
.
For example, the function below takes in an array of numbers, doubles each element and returns the sum of the doubled elements:
const doubleAndSum = (arr) => { let double = arr.map((a) => a * 2); return double.reduce((a, b) => a + b, 0); };
Below are the tests for the function:
test("Check if it doubles each element or the array and returns the sum of the doubled numbers", async (t) => { await t.test("Array of Length 3", (t) => { assert.strictEqual(doubleAndSum([1, 2, 3]), 12); }); await t.test("Array of Length 10", (t) => { assert.strictEqual(doubleAndSum([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]), 90); }); await t.test("Array of random large numbers", (t) => { assert.strictEqual( doubleAndSum([ 11, 24, 43, 4, 85, 6, 117, 8, 9, 510, 22, 4, 7, 9, 33, 45, 77, ]), 2028 ); }); });
Notice that the function is asynchronous, and all the tests are awaited with the await
keyword. This is because t.test
returns a promise, and without the await
keyword, the test will complete without all the subtests being executed.
t.test
mirrors the top-level test
. Hence it is the same syntax; a string describing the test, a callback function containing test logic, and additional optional options object.
Note that if one subtest fails, the entire test fails too.
Skipping Tests with the Test Runner Module
Suppose you need to skip an individual test; you can achieve this by either passing the skip option to the test object or calling the skip
method on the test context, t
. You can also include a message that will be displayed when the test is skipped in the test output in either of the methods.
For example:
//passing the skip option to the test object, with a message test('skip this test and display a message', { skip: 'this test has been skipped' }, (t) => { // This code is never executed }); //passing the skip option to the test object, without a message test('skip this test', { skip: true }, (t) => { // This code is never executed }); //calling the skip method on the test context, without a message test("skip this test", (t) => { // This code is never executed t.skip(); }); test("skip this test and display a message", (t) => { // This code is never executed t.skip('This test is skipped'); });
Note that when using the test context t
to skip tests and there is some additional logic in the function, be sure to return
.
Conclusion
Although these features are an excellent addition to NodeJS, it is not advisable to use them in production as it still has the experimental flag. As such, NodeJS may incorporate code-breaking changes at any time.