Skip to content
aviral gupta

// B5.3 · ~31 min · Beginner

Tests with node:test and node:assert

After this lesson you can write tests for the functions your main.js exports, choose the right assertion from node:assert/strict, and read what a failing assertion tells you.

Lesson 3 of 5 in B5 Events, input and first tests

You will be able to

  • Write sync and async tests with test() for a function exported from main.js, and run them with node --test
  • Choose the assertion: equal or deepEqual, ok, match, and throws or rejects with an expected error
  • Read a failing assertion: its message, the actual and the expected value
  1. Warm-up · Activity 1 of 7

    Warm-up from B4.2: an async function throws. What does the program print?

    async function load() {
      throw new Error('no file');
    }
    
    const result = load();
    console.log(result instanceof Promise);
    result.catch((error) => console.log('rejected:', error.message));
  2. Predict · Activity 2 of 7

    Predict before you read on. node --test runs this file. What does its summary say?

    import {test} from 'node:test';
    
    test('returns false', () => false);
    
    test('compares 1 + 1 with 3', () => {
      1 + 1 === 3;
    });
    
    test('throws', () => {
      throw new Error('boom');
    });
  3. Practice · Activity 3 of 7

    tally returns a new object of counts. Fill in the assertion that compares it with the expected object.

    assert.____(tally(['tea', 'milk', 'tea']), {tea: 2, milk: 1}, 'tally should count each word');
    assert.(tally(['tea', 'milk', 'tea']), {tea: 2, milk: 1}, 'tally should count each word');
  4. Practice · Activity 4 of 7

    Match each assertion to what it checks.

  5. Practice · Activity 5 of 7

    Read the real output of node --test for this main.js. Which value did toCents('0.29') return?

    // main.js
    // Turns a price like '3.50' into whole cents.
    export function toCents(text) {
      if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
      return Math.floor(Number(text) * 100);
    }
    
    // main.test.js
    import {test} from 'node:test';
    import assert from 'node:assert/strict';
    import {toCents} from './main.js';
    
    test('3.50 is 350 cents', () => {
      assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350");
    });
    
    test('0.29 is 29 cents', () => {
      assert.equal(toCents('0.29'), 29, "toCents('0.29') should be 29");
    });
    
    test('a word is not a price', () => {
      assert.throws(() => toCents('abc'), RangeError, "toCents('abc') should throw a RangeError");
    });
    
    // $ node --test   (durations, blank lines and the stack trace left out)
    // ✔ 3.50 is 350 cents
    // ✖ 0.29 is 29 cents
    // ✔ a word is not a price
    // ℹ tests 3
    // ℹ pass 2
    // ℹ fail 1
    // ✖ failing tests:
    // test at main.test.js:9:1
    // ✖ 0.29 is 29 cents
    //   AssertionError [ERR_ASSERTION]: toCents('0.29') should be 29
    //   28 !== 29
    //     actual: 28,
    //     expected: 29,
    //     operator: 'strictEqual',
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Four assertions, each run on its own. How many of them fail?

    import assert from 'node:assert/strict';
    
    const results = [];
    const check = (fn) => {
      try {
        fn();
        results.push('pass');
      } catch {
        results.push('FAIL');
      }
    };
    check(() => assert.equal(NaN, NaN, 'NaN'));
    check(() => assert.equal(0, -0, 'zero'));
    check(() => assert.deepEqual([1], [1], 'deep'));
    check(() => assert.equal([1], [1], 'same array?'));
    console.log(results.filter((r) => r === 'FAIL').length);
  7. Apply · Activity 7 of 7

    Mini-task. In a new folder with "type": "module" in package.json, write main.js exporting countWords(text) and an async countFileWords(path) that reads a file with node:fs/promises (B3.2). Write main.test.js with four tests: words split by any spaces, a text of only spaces, a file words.txt with 4 words, and a missing file that rejects with code ENOENT. Run node --test. Then break countWords on purpose and read the failure.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

What test() does, by hand

Before node --test runs a whole file, see what it does for one test. check(name, fn) calls fn and reports pass, or FAIL with the error, just as test() counts a throw as a failure. The assertions inside are the real ones from node:assert/strict: equal, deepEqual, throws with a class and with an object, ok and match. One check fails on purpose: equal on two objects.

main.js

import assert from 'node:assert/strict';

// Turns a price like '3.50' into whole cents.
export function toCents(text) {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
  return Math.round(Number(text) * 100);
}

// What test() does for you: run fn, and count a throw as a failure.
function check(name, fn) {
  try {
    fn();
    console.log('pass', name);
  } catch (error) {
    console.log('FAIL', name, '->', error.name + ':', error.message.split('\n')[0]);
  }
}

check('3.50 is 350 cents', () => {
  assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350");
});
check('0.29 is 29 cents, not 28', () => {
  assert.equal(toCents('0.29'), 29, "toCents('0.29') should be 29");
});
check('every price in a list', () => {
  assert.deepEqual(['1', '2.5'].map(toCents), [100, 250], 'each price in cents');
});
check('equal on two objects', () => {
  assert.equal({cents: 100}, {cents: 100}, 'two object literals are two objects');
});
check('a word is not a price', () => {
  assert.throws(() => toCents('abc'), RangeError, "toCents('abc') should throw a RangeError");
});
check('the error names the input', () => {
  assert.throws(() => toCents('3,50'), {name: 'RangeError', message: /3,50/}, 'the message should contain 3,50');
});
check('ok and match', () => {
  assert.ok(toCents('0') === 0, "toCents('0') should be 0");
  assert.match('not a price: x', /^not a price/, 'the message should start with not a price');
});

Run it with

node main.js

Output

pass 3.50 is 350 cents
pass 0.29 is 29 cents, not 28
pass every price in a list
FAIL equal on two objects -> AssertionError: two object literals are two objects
pass a word is not a price
pass the error names the input
pass ok and match
  • Each failed assertion threw an AssertionError whose message began with the last argument.
  • equal failed on two objects with the same contents; deepEqual passed on the arrays.
  • throws got a function, () => toCents('abc'), so it could call it and catch the error itself.
  • The failing check did not stop the others: each check catches its own error, as each test does.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Exercises

Exercise 1 of 2

Assertions that catch a bug

Write the assertions of a test as a function: checkToCents(toCents) must throw an AssertionError when the given toCents breaks a rule, and do nothing when it is correct. The rules: toCents('7') is 700, toCents('3.50') is 350, toCents('0.29') is 29, all numbers, and toCents('abc') throws a RangeError. The tests run your check against a correct toCents and against broken ones. Give every assertion a message.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    One assertion per rule: assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350"); equal is strict, so '350' fails it.

  2. Hint 2

    Add the same for '0.29' and 29: that one catches Math.floor.

  3. Hint 3

    For abc: assert.throws(() => toCents('abc'), RangeError, '…'). Pass a function; with RangeError as the class, a TypeError fails too.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import assert from 'node:assert/strict';

// Throws an AssertionError when toCents breaks one of its rules.
export function checkToCents(toCents) {
  assert.equal(toCents('7'), 700, "toCents('7') should be 700");
  assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350");
  assert.equal(toCents('0.29'), 29, "toCents('0.29') should be 29");
  assert.throws(() => toCents('abc'), RangeError, "toCents('abc') should throw a RangeError");
}

// A correct toCents passes the check.
checkToCents((text) => {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
  return Math.round(Number(text) * 100);
});
console.log('the correct toCents passed');
Run it on your computer

Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

import assert from 'node:assert/strict';

// Throws an AssertionError when toCents breaks one of its rules.
export function checkToCents(toCents) {
  assert.equal(toCents('7'), 700, "toCents('7') should be 700");
  // Add the other rules: '3.50', '0.29' and 'abc'.
}

// A correct toCents passes the check.
checkToCents((text) => {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
  return Math.round(Number(text) * 100);
});
console.log('the correct toCents passed');

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {checkToCents} from './main.js';

// A correct toCents, and broken ones with one bug each.
const good = (text) => {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
  return Math.round(Number(text) * 100);
};
const floors = (text) => (good(text), Math.floor(Number(text) * 100));
const strings = (text) => String(good(text));
const noThrow = (text) => (/^\d+(\.\d{1,2})?$/.test(text) ? good(text) : NaN);
const wrongError = (text) => {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new TypeError('not a price: ' + text);
  return good(text);
};
const catches = (broken) => {
  try {
    checkToCents(broken);
  } catch (error) {
    return error.name;
  }
  return 'nothing';
};

test('your check accepts a correct toCents', () => {
  checkToCents(good);
});

test('your check catches Math.floor, which turns 0.29 into 28', () => {
  assert.equal(catches(floors), 'AssertionError', 'checkToCents(floors) should throw an AssertionError');
});

test('your check catches a toCents that returns strings', () => {
  assert.equal(catches(strings), 'AssertionError', 'checkToCents(strings) should throw an AssertionError');
});

test('your check catches a toCents that returns NaN instead of throwing', () => {
  assert.equal(catches(noThrow), 'AssertionError', 'checkToCents(noThrow) should throw an AssertionError');
});

test('your check catches a toCents that throws a TypeError', () => {
  assert.equal(catches(wrongError), 'AssertionError', 'checkToCents(wrongError) should throw an AssertionError');
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

Assertions for an async function

Now async: checkFindUser(findUser) must reject with an AssertionError when findUser breaks a rule, and resolve when it is correct. The rules: await findUser(1) gives exactly {id: 1, name: 'Ada'}, await findUser(2) gives exactly {id: 2, name: 'Lin'}, with nothing more, and findUser(99) rejects with an error whose message is no user 99. The starter checks only a name. Give every assertion a message.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Compare whole objects with deepEqual: assert.deepEqual(await findUser(1), {id: 1, name: 'Ada'}, '…'). It is strict, so '1' is not 1 and an extra field fails.

  2. Hint 2

    For 99: await assert.rejects(findUser(99), {message: 'no user 99'}, '…'). Without await, the check would finish before the promise rejects.

  3. Hint 3

    checkFindUser is async, so a failed assertion inside rejects its promise: that is the failure the tests look for.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import assert from 'node:assert/strict';

// Rejects with an AssertionError when findUser breaks one of its rules.
export async function checkFindUser(findUser) {
  assert.deepEqual(await findUser(1), {id: 1, name: 'Ada'}, 'findUser(1) should give {id: 1, name: Ada}');
  assert.deepEqual(await findUser(2), {id: 2, name: 'Lin'}, 'findUser(2) should give {id: 2, name: Lin}');
  await assert.rejects(findUser(99), {message: 'no user 99'}, 'findUser(99) should reject with no user 99');
}

// A correct findUser passes the check.
const users = {1: 'Ada', 2: 'Lin'};
await checkFindUser(async (id) => {
  if (!(id in users)) throw new Error('no user ' + id);
  return {id, name: users[id]};
});
console.log('the correct findUser passed');
Run it on your computer

Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

import assert from 'node:assert/strict';

// Rejects with an AssertionError when findUser breaks one of its rules.
export async function checkFindUser(findUser) {
  const user = await findUser(1);
  assert.equal(user.name, 'Ada', 'findUser(1) should be called Ada');
}

// A correct findUser passes the check.
const users = {1: 'Ada', 2: 'Lin'};
await checkFindUser(async (id) => {
  if (!(id in users)) throw new Error('no user ' + id);
  return {id, name: users[id]};
});
console.log('the correct findUser passed');

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {checkFindUser} from './main.js';

// A correct findUser, and broken ones with one bug each.
const users = {1: 'Ada', 2: 'Lin'};
const good = async (id) => {
  if (!(id in users)) throw new Error('no user ' + id);
  return {id, name: users[id]};
};
const stringId = async (id) => ({...(await good(id)), id: String(id)});
const extraField = async (id) => ({...(await good(id)), admin: false});
const noReject = async (id) => (id in users ? good(id) : undefined);
const otherMessage = async (id) => {
  if (!(id in users)) throw new Error('not found');
  return good(id);
};
const catches = async (broken) => {
  try {
    await checkFindUser(broken);
  } catch (error) {
    return error.name;
  }
  return 'nothing';
};

test('your check accepts a correct findUser', async () => {
  await checkFindUser(good);
});

test('your check catches an id that is a string', async () => {
  assert.equal(await catches(stringId), 'AssertionError', 'checkFindUser(stringId) should reject with an AssertionError');
});

test('your check catches an extra field', async () => {
  assert.equal(await catches(extraField), 'AssertionError', 'checkFindUser(extraField) should reject with an AssertionError');
});

test('your check catches an unknown id that does not reject', async () => {
  assert.equal(await catches(noReject), 'AssertionError', 'checkFindUser(noReject) should reject with an AssertionError');
});

test('your check catches the wrong error message', async () => {
  assert.equal(await catches(otherMessage), 'AssertionError', 'checkFindUser(otherMessage) should reject with an AssertionError');
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Using test without importing it

import assert from 'node:assert/strict';

function toCents(text) {
  return Math.round(Number(text) * 100);
}

test('3.50 is 350 cents', () => {
  assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350");
});

What Node.js prints

ReferenceError: test is not defined

Why, and the fix

Some other test tools make test a global. In Node.js it is not: test comes from the node:test module, and only with the node: prefix. Add import {test} from 'node:test'; at the top, next to the assert import. The same goes for assert: import it from 'node:assert/strict'.

Calling the function inside assert.throws

import assert from 'node:assert/strict';

function toCents(text) {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
  return Math.round(Number(text) * 100);
}

assert.throws(toCents('abc'), RangeError, "toCents('abc') should throw a RangeError");
console.log('all checks passed');

What Node.js prints

RangeError: not a price: abc

Why, and the fix

assert.throws(toCents('abc'), …) calls toCents before throws even starts, so the RangeError flies out of that line and the program crashes. throws must call the function itself to catch the error: pass a function, assert.throws(() => toCents('abc'), RangeError, '…'). The same holds for assert.rejects with an async function, or pass it the promise and await it.

Testing a function that is not exported

import assert from 'node:assert/strict';
import {toCents} from './prices.js';

assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350");
console.log('all checks passed');

What Node.js prints

SyntaxError: The requested module './prices.js' does not provide an export named 'toCents'

Why, and the fix

A test file, like any module, can only import what the other module exports. Here prices.js declares toCents but does not export it, so the import fails before a single test runs; node --test then reports the whole file as failed. Write export function toCents(text) in the module you test.

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

A test is a function that throws when something is wrong

Import test from 'node:test' (only with the node: prefix) and write test('3.50 is 350 cents', () => { … }) in a file such as main.test.js. A test fails when its function throws, or, for an async function, when its promise rejects; otherwise it passes. Returning false or writing a comparison that nobody checks does not fail a test: an assertion must throw. Tests import the functions that main.js exports. node --test, with no further arguments, finds such test files, runs them, prints ✔ or ✖ per test and a summary, and ends with exit code 1 when a test failed. Nothing needs installing.

Pick the assertion that says what you mean

Import assert from 'node:assert/strict'; strict mode makes equal mean strictEqual. equal compares with Object.is: right for numbers and strings, but two objects or arrays are equal only if they are the same object. deepEqual compares structure and contents, strictly: {id: 1} is not {id: '1'}. ok checks that a value is truthy, match that a string matches a regular expression. throws(fn, expected) calls fn and expects an error; pass the function, do not call it. expected is a class, a RegExp tested against the error as text, or an object such as {message: /abc/}. For promises, await assert.rejects(promise, expected).

Read the failure

A failing assertion throws an AssertionError. Its message starts with the last argument you passed (Node.js 24 adds the comparison below it); without one, Node.js writes a default such as Expected values to be strictly equal:. node --test prints, for each failed test, the file and line, the line AssertionError [ERR_ASSERTION]: and your message, a short comparison such as 28 !== 29 (actual first), and the properties actual and expected. So write messages that say what should have happened, toCents('0.29') should be 29, and the output tells you the rest. Assertions in one test run in order; the first one that fails ends that test.

Sources

Last reviewed October 4, 2026