Testing (node:test)
Reviewed & published by Brayan K
A test is code that automatically checks your other code behaves as expected, and Node ships a built-in test runner — node:test — plus an assertion library, node:assert, with nothing to install.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll write tests with test() and assertions, compare values and objects, test code that throws, group tests with describe/it, handle async tests, and run everything with node --test.
What You'll Learn in This Lesson
- What an automated test is and why it's worth writing
- Writing a test with node:test and node:assert
- strictEqual vs deepStrictEqual for values and objects
- Asserting that code throws with assert.throws
- Grouping tests with describe() and it()
- Testing async code and running with node --test
1️⃣ Your First Test
A test pairs the test() function from node:test with an assertion from node:assert. The assertion throws if reality doesn't match your expectation; if nothing throws, the test passes. Run the file with node --test.
// A TEST is code that checks other code does what you expect — automatically.
// Node has a BUILT-IN test runner: the "node:test" module. No install needed.
const test = require("node:test");
const assert = require("node:assert"); // assertions: throw if a check fails
// The function we want to test.
function add(a, b) {
return a + b;
}
// test(name, fn) registers a test. The body uses assert to state expectations.
test("add() sums two numbers", () => {
// assert.strictEqual(actual, expected) passes if they're equal, throws if not.
assert.strictEqual(add(2, 3), 5);
assert.strictEqual(add(-1, 1), 0);
});
// Run this file with: node --test
// (Node finds test files and reports pass/fail counts.)2️⃣ The Everyday Assertions
Different checks call for different assertions: strictEqual for primitives, deepStrictEqual to compare objects and arrays by value, ok for truthiness, and throws to assert that code raises an error.
// Different checks need different assertions. Here are the everyday ones.
const test = require("node:test");
const assert = require("node:assert");
function makeUser(name) {
return { name, active: true };
}
test("strictEqual checks primitives", () => {
assert.strictEqual(2 + 2, 4);
});
test("deepStrictEqual checks objects/arrays by value", () => {
// Objects are equal here by their CONTENTS, not by reference.
assert.deepStrictEqual(makeUser("Sam"), { name: "Sam", active: true });
});
test("ok checks something is truthy", () => {
assert.ok(makeUser("Sam").active); // passes because active is true
});
test("throws checks that code throws", () => {
// The function passed in is EXPECTED to throw.
assert.throws(() => { throw new Error("boom"); });
});The trap to remember: strictEqual on two separate objects with identical contents fails, because === compares references. Use deepStrictEqual for structured data.
3️⃣ Grouping and Async Tests
Use describe() to group related cases under a heading and it() to name each one. Tests can be async: make the function async and await inside — the runner waits before judging.
// Use describe() to GROUP related tests, and it() to name each case.
// Tests can be async — just return a promise or use an async function.
const { describe, it } = require("node:test");
const assert = require("node:assert");
function divide(a, b) {
if (b === 0) throw new Error("Cannot divide by zero");
return a / b;
}
async function fetchScore() {
// Pretend this hits a database; it resolves after a tick.
return Promise.resolve(42);
}
describe("divide()", () => {
it("divides normally", () => {
assert.strictEqual(divide(10, 2), 5);
});
it("throws on divide by zero", () => {
assert.throws(() => divide(1, 0), /Cannot divide by zero/);
});
});
describe("fetchScore()", () => {
it("resolves to a number (async)", async () => {
const score = await fetchScore(); // await works inside an async test
assert.strictEqual(score, 42);
});
});// A test report is not a fixed string — it carries a duration per test, so
// it reads differently every run (see the note on the blocks above). The
// ASSERTIONS underneath it are completely predictable, so here they are
// called directly, with a tiny runner that does what every test framework
// does for you: call the check, catch what it throws, count it.
const assert = require('node:assert/strict');
function check(name, fn) {
try { fn(); console.log('PASS ' + name); }
catch (err) { console.log('FAIL ' + name + ' -> ' + err.code); }
}
check('strictEqual: 2 + 2 is 4', () => assert.strictEqual(2 + 2, 4));
check('strictEqual: 0.1 + 0.2 is not 0.3', () => assert.strictEqual(0.1 + 0.2, 0.3));
check('deepStrictEqual compares by value', () => assert.deepStrictEqual({ a: [1, 2] }, { a: [1, 2] }));
check('strictEqual compares objects by identity', () => assert.strictEqual({ a: 1 }, { a: 1 }));
check('ok accepts anything truthy', () => assert.ok('non-empty'));
check('throws catches the error it expects', () => assert.throws(() => { throw new RangeError('nope'); }, RangeError));
console.log('');
console.log('0.1 + 0.2 really is', 0.1 + 0.2);
// The two FAILs are the point, and they are the two that catch people out:
//
// 0.1 + 0.2 is not 0.3 in any language with binary floating point. Never
// assert exact equality on a computed float — compare to a tolerance.
//
// strictEqual on two objects asks "are these the SAME object?", and two
// freshly-built objects never are, however identical they look. Use
// deepStrictEqual when you mean "do these hold the same values?".
//
// Every assertion throws an AssertionError with code ERR_ASSERTION, which is
// exactly how the test runner knows a test failed.Your turn. Fill in the two ___ blanks to complete the tests for isEven, then run them.
// 🎯 YOUR TURN — finish the tests, then run: node --test
const test = require("node:test");
const assert = require("node:assert");
function isEven(n) {
return n % 2 === 0;
}
test("isEven() returns true for even numbers", () => {
// TODO 1: assert that isEven(4) is true (use assert.ok or strictEqual).
assert.strictEqual(isEven(4), ___); // 👉 the boolean you expect
});
test("isEven() returns false for odd numbers", () => {
// TODO 2: assert that isEven(7) is false.
assert.___(isEven(7), false); // 👉 the assertion that compares values
});
// ✅ Expected:
// ✔ isEven() returns true for even numbers
// ✔ isEven() returns false for odd numbers
// ℹ pass 2Mini-Challenge: Test a Cart Total
Write three tests for the total() function — empty cart, one line, two lines — grouped with describe and it. Run them with node --test.
// 🎯 MINI-CHALLENGE: test a small "cart" function.
// Save as cart.test.js and run: node --test
//
// The function under test:
function total(items) {
// items is an array of { price, qty }
return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}
//
// Write THREE tests using node:test + node:assert:
// 1. total([]) is 0 (empty cart)
// 2. total([{price:2, qty:3}]) is 6 (one line)
// 3. total([{price:2,qty:1},{price:5,qty:2}]) is 12 (two lines)
// Group them with describe("total()", ...) and one it() each.
//
// ✅ Expected: 3 passing tests.
const { describe, it } = require("node:test");
const assert = require("node:assert");
// your tests hereCommon Errors (and the fix)
- Comparing objects with strictEqual fails — strictEqual uses === (reference equality). Use deepStrictEqual to compare contents.
- Async test passes when it shouldn't — You forgot to await (or return) the promise, so the test ended early. Make the function async and await the work.
- No tests are found — The runner looks for *.test.js or a test/ folder. Name the file accordingly, or pass it explicitly: node --test file.js.
- assert.throws "fails" on code that doesn't throw — Pass a function, e.g. () => divide(1, 0), not the already-called result.
- Old Node has no node:test — Update to Node 18 or newer; earlier versions don't include the built-in runner.
Pro Tips
- 💡 Name tests as behavior: "throws on divide by zero" reads like documentation and pinpoints failures fast.
- 💡 Watch mode: node --test --watch reruns tests automatically as you edit.
- 💡 One assertion idea per test: small, focused tests make a failure obvious instead of ambiguous.
📋 Quick Reference — node:test
| Goal | Code |
|---|---|
| Define a test | test("name", () => {}) |
| Compare primitives | assert.strictEqual(a, b) |
| Compare objects | assert.deepStrictEqual(a, b) |
| Assert truthy | assert.ok(value) |
| Assert it throws | assert.throws(() => f()) |
| Run all tests | node --test |
🎉 Lesson Complete!
- ✅ Tests automatically check your code does what you expect
- ✅ node:test and node:assert are built in — no install
- ✅ strictEqual for primitives, deepStrictEqual for objects
- ✅ assert.throws verifies error-raising code
- ✅ Group with describe/it; await async tests; run with node --test
Practice quiz
Which built-in modules let you write tests in modern Node with no install?
- node:jest and node:mocha
- node:test and node:assert
- node:check and node:spec
- node:runner and node:expect
Answer: node:test and node:assert. node:test is the runner and node:assert provides the assertions — both built in.
Which assertion compares objects and arrays by their contents?
- assert.deepStrictEqual
- assert.strictEqual
- assert.ok
- assert.equalRef
Answer: assert.deepStrictEqual. deepStrictEqual recurses through properties; strictEqual uses === (reference).
Why does assert.strictEqual({a:1}, {a:1}) fail?
- The syntax is wrong
- Objects aren't allowed
- It uses === which compares references, not contents
- It needs await
Answer: It uses === which compares references, not contents. strictEqual is reference equality; two separate objects are never ===.
How do you assert that a function throws an error?
- assert.fail(fn)
- assert.error(fn)
- assert.catch(fn)
- assert.throws(() => fn())
Answer: assert.throws(() => fn()). Pass a function to assert.throws; it passes if that function throws.
What command runs your tests with the built-in runner?
- node --test
- npm test
- node run tests
- node:test start
Answer: node --test. node --test finds test files and reports pass/fail counts.
What do describe() and it() do?
- They mock network calls
- They group related tests and name each case
- They run only failing tests
- They time the tests
Answer: They group related tests and name each case. describe() groups tests under a heading; it() defines a single named case.
How do you correctly test asynchronous code?
- Use setTimeout to wait
- Ignore the promise
- Make the test async and await the operation
- Call done() twice
Answer: Make the test async and await the operation. An async test function lets the runner await the promise before judging pass/fail.
Which assertion checks that a value is truthy?
- assert.true
- assert.is
- assert.truthy
- assert.ok
Answer: assert.ok. assert.ok(value) passes when value is truthy.
Where does the runner look for test files by default?
- Files like *.test.js or inside a test/ folder
- Only in src/
- Anywhere named main.js
- Only files you pass with --watch
Answer: Files like *.test.js or inside a test/ folder. node --test auto-discovers *.test.js files and a test/ directory.
What happens if you forget to await an async assertion?
- The test always fails
- Node refuses to run
- The test may finish early and a real failure slips through
- It retries forever
Answer: The test may finish early and a real failure slips through. Without await/return the test ends before the work, hiding real failures.
Continue this course
- Previous: Environment & Config
- Next: CommonJS vs ES Modules — require/module.exports vs import/export, .cjs/.mjs, and interop pitfalls
- Quick reference: Node.js cheat sheet
Frequently asked questions
Do I need a library like Jest or Mocha to test Node code?
No longer. Modern Node.js includes a built-in test runner in the node:test module and an assertion library in node:assert, so you can write and run tests with zero dependencies. You import test (or describe/it) and assert, write your checks, and run the files with node --test. Third-party frameworks like Jest still offer extras — rich mocking, snapshot testing, a large plugin ecosystem — but for most projects the built-in runner is enough and means one fewer dependency to manage.
What's the difference between strictEqual and deepStrictEqual?
assert.strictEqual compares with ===, which is right for primitives like numbers, strings, and booleans, and for checking that two variables are literally the same object. assert.deepStrictEqual compares objects and arrays by their contents, recursing through all properties — so { a: 1 } deepStrictEqual { a: 1 } passes even though they are two different objects in memory. Using strictEqual on two separate-but-identical objects fails, which surprises beginners; reach for deepStrictEqual whenever you compare structured data.
How do I test asynchronous code?
Make the test function async and await the operation inside it, then assert on the result. The node:test runner waits for the returned promise to settle before deciding pass or fail, so an awaited assertion is checked correctly. You can also test that an async function rejects using assert.rejects(promise). The key is to actually await or return the promise — if you forget, the test function finishes before the async work does, and a real failure can slip through as a false pass.
How do I run my tests, and what do describe and it do?
Run node --test in your project; Node automatically finds files named like *.test.js or inside a test/ folder, runs them, and prints a summary of passes and failures (exiting non-zero if any fail, which is what CI systems check). describe() groups related tests under a heading and it() defines a single test case with a readable name — they're just an organizational style layered on top of the basic test() function, producing nicely nested output that documents what your code is supposed to do.