The NodeJS Test Runner Module

1137 VIEWS

·

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.


David Ekete is a software developer, technical writer, and JavaScript / TypeScript developer with experience building scalable backend infrastructure for web applications interested in exploring and contributing to emerging technology.


Discussion

Click on a tab to select how you'd like to leave your comment

Leave a Comment

Your email address will not be published.

Menu
Skip to toolbar