Skip to content

Instantly share code, notes, and snippets.

@subfuzion
Last active August 29, 2023 01:41
Show Gist options
  • Save subfuzion/11013a8435c9de5302430fd44c3f76a7 to your computer and use it in GitHub Desktop.
Save subfuzion/11013a8435c9de5302430fd44c3f76a7 to your computer and use it in GitHub Desktop.
Node.js 18 Test Runner

Node.js 18 Test Runner

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.

1. Test any Node module that returns an exit code

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

2. Include tests using naming pattern

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 a test directory, recursively.

Where EXT is one of js|cjs|mjs.

3. Writing test functions

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

4. Using the TestContext

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 => {
  });
  
});

5. Simplifying with describe and it

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);
  });
});

6. Running a subset of tests

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.

@koresar
Copy link

koresar commented Aug 15, 2022

How would I run a single test from a suite?

@subfuzion
Copy link
Author

@koresar:

How would I run a single test from a suite?

I added section 6. Running a subset of tests

@koresar
Copy link

koresar commented Aug 18, 2022

Beautiful!

@koresar
Copy link

koresar commented Aug 18, 2022

@subfuzion I couldn't find a way to run a test by name. I mean, without changing JS code. Something like:

node --test-only test/test.js:"test suite":"nested test suite will run":"will succeed"

or even

node --test-only test/test.js:"will succeed"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment