Skip to main content

Unified JavaScript Testing

Node.js now has a built-in test runner, which I've now tried, and it's fantastic! However, it can't be used for frontend testing, or browser testing, obviously. While web-test-runner is the best (imo) test runner for frontend tests, having different runners requires you to learn different libraries and use different imports for each even when doing dead simple environment-agnostic unit tests. In this post I'll talk about how I've unified the experience, and the path that lead to being able to do that.

Node.js built-in test runnerโ€‹

First of all, I'll discuss how Node.js now has a built-in test runner, which is a dream to use. It doesn't require a huge config, and tsx makes it a breeze to use with TypeScript. I much prefer this over Mocha (which I used to use for backend testing).

Any suite functions you likeโ€‹

The Node.js test runner exports multiple test suite functions, so you can pick your favorite:

  • describe()
  • suite()
  • it()
  • test()

(Personally I prefer describe() and it() because it makes reading the test descriptions in the code more smooth.)

In a huge leap over Mocha, these suite functions are not globals. They are imported from node:test. (Note that you must include the node: prefix for this import.)

import {describe, it, suite, test} from 'node:test';

All-in-one test runnerโ€‹

The Node.js's test runner includes everything you need in a test runner:

Note that several of those all-in-one features are still experimental (noted above by ๐Ÿงช) and thus are not currently stable. Here are my experiences trying out those experimental features:

  • Code coverage calculations are not accurate for TypeScript. I'm using c8 instead.
  • Watch mode works fine.
  • Snapshot testing works fantastically, though it's still missing types in @types/node.

How to run testsโ€‹

It's super straightforward:

node --test 'src/**/*.test.js'

Running tests for TypeScript files isn't any harder:

tsx --test 'src/**/*.test.ts'

What a test looks like in the Node.js built-in test runnerโ€‹

Nothing unfamiliar here! Node.js also includes its own assert library, but I recommend Chai instead as it has more features. (Chai also supports ESM now, more on that later.)

import assert from 'node:assert/strict'; // make sure to import the strict version
import {describe, it} from 'node:test';
import {myFunction} from './index.js';

describe('my test', () => {
it('does something', async () => {
assert.deepStrictEqual(await myFunction(), {something: 'to test'});
});
});

Why I like the built in Node.js test runnerโ€‹

I've been using Mocha for backend testing for a few years now, and never really liked it much. However, I liked all the other options less! The Node.js test runner has been great though and now I'm completely switching to it in all my projects. Here's why:

  • support for describe and it
  • test functions (like describe and it) are not globals
  • built-in really great snapshot testing
  • no cumbersome config file
  • super clear documentation
  • no need for another dependency
  • built-in support from tsx for TypeScript
  • incredible Date mocking capabilities (though I haven't tried it yet)

web-test-runnerโ€‹

I've already talked about how much I love the web-test-runner testing package in a previous blog post, but I have to mention it here as a major contributor to my joy with the current JavaScript testing ecosystem. To summarize my thoughts from the previous blog post, web-test-runner is great because it runs frontend unit tests in an actual browser and lets you run them in all the major browsers.

Universal Chaiโ€‹

Chai's assert function has always been one of my favorites (intentionally leaving out the confusing expect tester). There are no changes to its interface in modern Chai, but Chai is now, as of 7 months ago, exported in ESM. Thus, it can now be used directly in both the backend and the frontend without any fancy build processes or extra steps.

It still doesn't include it's own TypeScript types though.

@augment-vir/testโ€‹

Tying all of these great improvements in the testing ecosystem is my new, still unpublished, @augment-vir/test package. This package has the following features:

  • exports a describe testing function which automatically detects which environment its running in (backend vs frontend) and dynamically calls the correct test runner describe based on that (Node.js's built-in test runner for the backend and web-test-runner for the frontend). (Still unreleased code here.)
  • includes itCases and it alongside each other
  • no more separate @augment-vir/chai and @augment-vir/browser-testing packages
  • masks the Mocha describe export so you don't need to rely on globals
  • re-exports Chai's assert (so you don't need to separately install Chai), intentionally leaving out expect (so you don't have to worry about its issues), and includes types (so you don't need to separately install @types/chai)

Now you can import everything for testing in both frontend and backend unit testing from a single package!

The only strangeness with this package is that it is no longer an import, but extracted from the describe callback's parameters, as you'll see in the example below. It's not a problem, it's just different than normal.

What testing with @augment-vir/test looks likeโ€‹

Here's the same test example from the above Node.js test runner section but using @augment-vir/test:

import {assert, describe} from '@augment-vir/test';
import {myFunction} from './index.js';

describe('my test', ({it}) => {
it('does something', async () => {
assert.deepStrictEqual(await myFunction(), {something: 'to test'});
});
});

It's not a huge difference, but we now have the following characteristics of our tests:

  • they can be run in frontend or backend test runners without changing any code or writing separate test files
  • all test setup is imported from a single place

This allows running tests in both environments at the same time for maximum testing coverage (like in the new @augment-vir/common's test command) with no extra boilerplate in test files (as can be seen here).

Running tests with @augment-vir/testโ€‹

@augment-vir/test is not a test runner, so you use the same commands for running Node.js's test runner or web-test-runner as always:

  • tsx --test 'src/**/*.test.ts'
  • web-test-runner --config configs/web-test-runner.mjs

Or, you can use virmator as shown below.

Unified test runnerโ€‹

The last piece of the puzzle is unifying the unit test runner. This can be done with my virmator package. This package will use Node.js's built-in test runner for backend tests and web-test-runner for frontend tests, with default configurations for a pleasant experience. It's very simple:

  • virmator test node
  • virmator test web

Each command automatically sets useful flags, configs, file globs, and allows passing in an individual file path for narrowing test focus.