Skip to content
Theme:

You might not need Jest — the Node.js native test runner is great

You might not need Jest. Just use Vitest instead. Quick one today, bye!

It was just a joke! It is good advice, although today, I would like to share some of my findings from early explorations of the native Node.js test runner. As a disclaimer note, I will not play the feature comparison game between the Node.js module and your favourite framework, so please spare your comments about missing features. This post aims to show you that sometimes the native test runner is more than enough.

Test runner module

Modern JavaScript runtimes, such as Deno and Bun, come equipped with testing capabilities, as do modern programming languages like Go and Rust. Node.js is catching up, and many of highly-requested features landed in the last couple of releases. Command-line arguments parser and watch mode are probably my favourite ones, but there is more. The experimental version of the test runner module landed in version 18 and hit stable in version 20.

Basic usage

The built-in node:test fails and returns process exit code set to 1 if the inner operation throws an error, rejects the promise or a callback is called with an error (first argument). It passes, and returns process code 0 otherwise. For the sake of simplicity, allow me to skip callback flavour examples as async/await functions and promise-based APIs are more common nowadays. I will use the built-in node:assert module to simplify assertions. Let’s have a look at a few examples.

// index.test.js

import test from "node:test";
import { deepEqual } from "node:assert";

test("passing test 1", () => {
  deepEqual(1 + 1, 2);
});

test("passing test 2", async () => {
  await new Promise((resolve) => resolve());
});

test("failing test 1", () => {
  deepEqual(1 + 1, 3);
});

test("failing test 2", async () => {
  await new Promise((_, reject) => reject());
});

Executing node --test runs tests against all matching files according to the test runner execution model.

  1. files explicitly passed as argument
  2. files in the test directory
  3. test.{js,mjs,cjs}
  4. test-*.{js,mjs,cjs}
  5. *.test.{js,mjs,cjs}, *-test.{js,mjs,cjs}, *_test.{js,mjs,cjs}

Grouping, skipping, mocking, spying, hooks and more

At first, it was limited when testing facilities were added to Node.js. Fast-forward a few iterations, and we now have access to many more frequently used testing features, well-known from other frameworks like Jest or Mocha. Look at a more complex example that uses test grouping, mocking, hooks, and other goodies.

import { describe, test, mock, before, after } from "node:test";
import { deepEqual } from "node:assert";
import os from "node:os";

const msg = (name) => `${name}, your computer has ${os.cpus().length} CPUs.`;

// grouping
describe("msg", () => {
  // hooks
  before(() => console.log("I run before a test suite."));
  after(() => console.log("I run after a test suite."));

  test("generates message", () => {
    deepEqual(msg("Pawel"), "Pawel, your computer has 8 CPUs.");
  });

  test("generates message (mocked data)", () => {
    // mocking/spying
    const osCpusMock = mock.method(os, "cpus", () => ({
      length: 666,
    }));

    deepEqual(msg("Dan"), "Dan, your computer has 666 CPUs.");
    deepEqual(osCpusMock.mock.callCount(), 1);
  });
});

// skipping tests
describe("failing", { skip: true }, () => {
  test("failing test 1", () => {
    deepEqual(1 + 1, 3);
  });

  test("failing test 2", async () => {
    await new Promise((_, reject) => reject());
  });
});

Watch mode

After adding the watch mode to Node.js CLI in version 18.11, the watch mode for test runner was added slightly later in version 19.2 (it is still experimental when writing this article).

node --test --watch

Custom code reporters

The default human-readable test reporter (spec) can be replaced with a compact (dot) format. If neither of these options suits your needs, custom reporters can be installed using npm. If that is not sufficient, creating a custom reporter that follows the TAP (Test Anything Protocol) specification is an option.

Or…

Or use jest, it is fine!

Comments

  • J
    Jeff Wainwright

    Love this article and am excited to eventually only use node's test and assert libs for unit testing. One or more like 1000 less node_modules to install sounds great!

    After spending time switching projects to Vitest from Jest, I found there were things, like mocking, where Jest was faster to setup. Have you had any similar experiences with node's test and assert libs?

    Thanks for the great article!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      I don't have a thorough comparison between the native test runner and Jest/Vitest yet. I've just started playing around with it to see if I can start incorporating it into some of my smaller projects.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • B
    Brandon Zhang

    Thanks for the introduction Pawel. I just started to play with it on this project of mine: https://github.com/heybran/freeinvoicebuilder/blob/main/test/index.test.js

    It does feel good to use built-in tools from Node instead of a testing library.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!

Leave a comment

👆 you can use Markdown here

Your comment is awaiting moderation. Thanks!