NodeJS: We can run tests natively!

NodeJS: We can run tests natively!

Once upon a time, there was Mocha.

Then there was Jasmine. Or perhaps it was the other way around. But Mocha was first for me.

There was Jest.

Now there's Vitest.

If you want to write tests for your Nodejs app, you're spoilt for choice. You just run npm install for the pleasure.

Some worked well with Grunt and Gulp.

Others worked great with Webpack.

Now there are those that work perfectly with Vite.

But unless we're doing some compilation for the front end or we're working with Typescript, why aren't we using NodeJS's native test runner? It's been around since version 18! That's April 2022. Almost 2 years in!

So what does it look like? 💯 zero node modules required:

And straight from the docs.

// > node test.mjs
import test from 'node:test';
import assert from 'node:assert';

test('synchronous passing test', (t) => {
    assert.strictEqual(1, 1);

That's it!

Assertions? They're native.

Test runners? Also native.

The output? Delightful. 🤩

✔ synchronous passing test (0.618667ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 4.6255

We can even run tests spanning multiple files. As long as the file ends with an acceptable glob pattern like *.test.mjs , you can run the following:

node --test

Default globs are:

  • **/*.test.?(c|m)js

  • **/*-test.?(c|m)js

  • **/*_test.?(c|m)js

  • **/test-*.?(c|m)js

  • **/test.?(c|m)js

  • **/test/**/*.?(c|m)js

We even have watch mode for goodness' sake. How did I not know about this?

node --test --watch

Code coverage? Yes. 😯

# node --test --experimental-test-coverage

ℹ tests 6
ℹ suites 2
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 54.586833
ℹ start of coverage report
ℹ --------------------------------------------------------------
ℹ file          | line % | branch % | funcs % | uncovered lines
ℹ --------------------------------------------------------------
ℹ test.mjs      | 100.00 |   100.00 |  100.00 | 
ℹ test.test.mjs | 100.00 |   100.00 |  100.00 | 
ℹ --------------------------------------------------------------
ℹ all files     | 100.00 |   100.00 |  100.00 |
ℹ --------------------------------------------------------------
ℹ end of coverage report

describe/it blocks? Yes, too! 🤯

describe('A thing', () => {
    it('should work', () => {
        assert.strictEqual(1, 1);

    it('should be ok', () => {
        assert.strictEqual(2, 2);

    describe('a nested thing', () => {
        it('should work', () => {
            assert.strictEqual(3, 3);

And it looks beautiful may I add.

▶ A thing
  ✔ should work (0.082667ms)
  ✔ should be ok (0.051292ms)
  ▶ a nested thing
    ✔ should work (0.05575ms)
  ▶ a nested thing (0.126541ms)

▶ A thing (0.512584ms)

So why aren't we using more of Nodejs natively?

One reason is that the test runner didn't exist until April 2022, probably when I was already super excited about Vite. Another reason is we rarely just work with pure Nodejs. Normally we work with a frontend framework, be it React or Vue, Typescript, Purescript, or a fully-fledged framework like AdonisJS to make our life easier.

Reducing our dependencies and relying on native

Having recently fallen foul of dependency hell with an old project, I came to the realisation, that reducing dependencies makes it easier to update a project. Try and fix a Node version 8 script with a dozen node modules that are no longer maintained, and you realise it's probably easier to just write the whole thing again.

The downside is native Nodejs will be more verbose to write, it will lack the Typescript types that I love so much, and it will be missing the prettier documentation that comes with beautiful frameworks like Nuxt.

However, if I want my projects to last a long time with a minimum of fuss and maintenance, it might be worth forgoing these shinier tools that abstract away NodeJS's NodeJSness, and just use it with its fullest purity.

It's not just a NodeJS problem. Every language has this problem. We've developed tools to make our lives easier because changing and improving the underlying language isn't always possible.

Yet, Ricardo Lopes proved you can have a sane zero-dependency mini web application without even a package.json written in NodeJS:

"There's a place for complex frameworks and architectures, sure. But for many projects, they may be an overkill."

So if we're developing a project that's 1) small, 2) not really going to change much, and 3) only needs NodeJS, why not avoid using any dependencies at all, and go native with node --test?

I'll sign off with a Flavio Tweet in favour of reducing dependencies.

Did you find this article valuable?

Support Gemma Black by becoming a sponsor. Any amount is appreciated!