Testing with the Built-in Test Runner
By Syncfusion®, Inc. · Last updated
Why tests matter, unit and integration tests, Arrange-Act-Assert, node:test with test, describe and it, running tests with node --test, test file discovery, subtests, async tests, skip, todo and only, timeouts and reporters, node:assert strict mode, hooks, mocking functions, methods and timers, testing file-system code, EventEmitters, streams, HTTP servers and CLI tools, test coverage, watch mode, and how node:test compares with Vitest and Jest.
Testing with the Built-in Test Runner
Test = a small program that checks that your code does what it should.
- You write the code once, and the tests check it again and again.
- Node.js has a test runner built in:
node:test, with checks fromnode:assert. - No extra package is needed.
Simple lesson: a test is code that checks your code, so you do not have to check by hand.
A Real-World Example
Think of the exam answer key.
- The teacher writes the right answers once.
- Every paper is checked against the key.
- If a paper says 2 + 2 = 5, the key catches it at once.
- Next year, the same key checks new papers in minutes.
Tests are the answer key for your code.
What You Will Learn
In this lesson, you will learn:
- Why tests matter, and the Arrange-Act-Assert pattern
test(),describe()andit(), and runningnode --test- Subtests, async tests,
skip,todo,onlyand timeouts - Assertions with
node:assert/strict - Hooks:
before,after,beforeEach,afterEach - Mocks for functions, methods and timers
- Testing files, events, streams, HTTP servers and CLI tools
- Coverage, watch mode and isolation
node:testcompared with Vitest and Jest
Testing Fundamentals
- Why test? You change code without fear, because tests tell you at once if something broke.
- Unit test = checks one small piece, such as one function, on its own.
- Integration test = checks several pieces working together, such as a server and its routes.
Arrange-Act-Assert = the three steps inside a test:
- Arrange = prepare the data.
- Act = call the code.
- Assert = check the result.
Simple lesson: prepare, call, check. Every good test follows these three steps.
Animation: the test cycle. Write a failing test, make it pass, then clean up the code 1. Redthe test fails 2. Greenfix code, test passes 3. Clean uptests still pass
node --test runs every *.test file and reports each result
Red, green, clean up. Then repeat for the next small feature.
node:test Basics
test(name, fn)= one test. It passes iffnfinishes without throwing.describe(name, fn)= a group of tests.it= another name fortest, used inside groups.node --test= find test files and run them.- Test files are found by name, for example
grades.test.js,grades-test.js,test.jsand every file in atest/folder.
Click ▶ Run. The lab runs node --test.
- ✔ = a passing test. ▶ = a group, with its tests indented under it.
- The summary counts tests, groups (suites), passes and failures.
- Exit code = 0 when every test passes, 1 when any test fails. Build servers use this.
When you fix a bug, write the test first. Watch it fail, fix the code, and watch it pass.
A Failing Test
450 !== 440= actual result first, expected value second.- The test found a bug in
fees.ts. Change0.125to0.1, and run again.
More Test Options
- Subtests =
t.test(name, fn)inside a test. Await them. - Async tests = make the function
async. The test waits for it. skip= do not run this test now:test.skip(...)or{ skip: true }.todo= a test you plan to finish later. It runs, but a failure does not count.only= run only the marked tests, withnode --test --test-only.- Timeouts =
{ timeout: 1000 }fails a test that takes longer than 1 second. - Reporters = the output style.
--test-reporter=specis the readable tree.tapanddotalso exist.
Simple lesson: name each test by what it checks, and run them all with node --test.
Assertions
Import the strict version: import assert from "node:assert/strict".
equal(a, b)= same value, using===.deepStrictEqual(a, b)= same contents for objects and arrays.throws(fn, Error)= the function must throw.rejects(promise, /text/)= the promise must fail.match(text, /pattern/)= the text must match.ok(value)= the value must be truthy.
Simple lesson: use equal for values, deepStrictEqual for objects, and throws or rejects for errors.
Hooks
Hook = code that runs around your tests, to prepare and clean up.
before(fn)= once, before all tests in the group.after(fn)= once, after all tests in the group.beforeEach(fn)= before every test. Use it to give each test fresh data.afterEach(fn)= after every test.
Simple lesson: use beforeEach so tests never depend on each other.
Mocking
Mock = a fake stand-in for a real function, method or timer, so a test controls it and watches it.
mock.fn(impl)= a fake function that records every call infn.mock.calls.mock.method(obj, "name", impl)= replace one method on an object.mock.timers= fake time.enable(), thentick(ms)moves time forward at once.- Restore =
m.mock.restore()ormock.restoreAll()puts the real thing back. Inside a test,t.mockrestores by itself. - Module mocking =
t.mock.module(...)replaces a whole imported module. It needs an extra flag (--experimental-test-module-mocks).
Mock only the slow or costly parts. If you mock everything, the test checks nothing real.
Simple lesson: mocks replace slow, costly or random parts, so tests stay fast and sure.
Testing Node.js Code
- File-system code = work inside a fresh temp folder from
fs.mkdtemp, and delete it after. - EventEmitters =
await once(emitter, "event")and check the arguments. - Streams = collect the output with
await stream.toArray(), then compare. - HTTP servers =
listen(0)picks a free port. Call it withfetch, thenclose()the server. - CLI tools = run them with
execFileand check stdout and the exit code.
Testing a CLI tool runs a real child process. Here it is with real Node.js:
// main.mjs: a test that runs a CLI and checks its output
import { test } from "node:test";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const run = promisify(execFile);
test("the CLI greets by name", async () => {
const { stdout } = await run(process.execPath, ["-e", "console.log('Hello, ' + process.argv[1])", "Asha"]);
assert.equal(stdout.trim(), "Hello, Asha");
});
Output:
TAP version 13
# Subtest: the CLI greets by name
ok 1 - the CLI greets by name
---
duration_ms: 57.342021
type: 'test'
...
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 61.696107
Simple lesson: use temp folders, once, toArray, port 0 and execFile to test real Node.js code.
Coverage and Watch Mode
- Coverage = which lines your tests actually ran.
node --test --experimental-test-coverageprints a table. - Watch mode =
node --test --watchruns the tests again whenever you save a file. - Isolation = each test file runs in its own process by default, so files cannot disturb each other.
- Concurrency = files run in parallel.
--test-concurrency=1runs them one at a time.
Add "test": "node --test" to package.json, so npm test runs your tests.
grades.js
export function grade(mark) {
if (mark >= 90) return "A";
if (mark >= 35) return "Pass";
return "Fail";
}
grades.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { grade } from "./grades.js";
test("A for 95", () => assert.equal(grade(95), "A"));
package.json
{ "type": "module" }
$ node --test --experimental-test-coverage
ℹ start of coverage report
ℹ ---------------------------------------------------------------
ℹ file | line % | branch % | funcs % | uncovered lines
ℹ ---------------------------------------------------------------
ℹ grades.js | 60.00 | 66.67 | 100.00 | 3-4
ℹ grades.test.js | 100.00 | 100.00 | 100.00 |
ℹ ---------------------------------------------------------------
ℹ all files | 80.00 | 80.00 | 100.00 |
- The output above shows only the coverage table. The test lines come before it.
- The tests ran only some lines of
grades.js. The"Pass"and"Fail"lines were never checked.
node:test vs Vitest vs Jest
node:test= built in, no install, fast to start. Good for Node.js libraries, tools and APIs.- Vitest = popular with Vite, React and Vue projects. Shares Vite's settings (see Testing Vue Apps).
- Jest = older and very widely used, with many plugins.
- The ideas are the same in all three:
describe,it, assertions, hooks and mocks.
Simple lesson: measure coverage to find untested code, and pick the runner your project already uses.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
assert.equal on two objects | It compares contents | Use deepStrictEqual | The test fails though the data is the same |
| Not awaiting an async assertion | The test waits anyway | await assert.rejects(...) | The test passes before the check runs |
| Tests that share data | Order does not matter | Reset data in beforeEach | Tests pass alone but fail together |
| Calling real email or SMS services | Tests should be fully real | Mock them | Slow, costly, flaky tests |
| Waiting for real timers | Tests must wait for real time | Use mock.timers and tick | Slow tests |
| A fixed port like 3000 in tests | The port is always free | Use listen(0) | EADDRINUSE when tests run in parallel |
Leaving only in the code | It is just for now | Remove it before committing | Most tests silently stop running |
TL;DR
- Test = code that checks code. Arrange, Act, Assert.
test,describe,itfromnode:test. Run withnode --test.- Exit code = 0 all pass, 1 any fail.
skip,todo,only,timeout= control which tests run.- Assertions =
equal,deepStrictEqual,throws,rejects,match. - Hooks =
before,after,beforeEach,afterEach. - Mocks =
mock.fn,mock.method,mock.timers. - Node.js code = temp folders,
once,toArray, port 0,execFile. - Coverage =
--experimental-test-coverage. Watch =--watch.
Check Yourself
Exercises
What to Remember
- A test arranges data, acts, and asserts the result.
node --testfinds and runs test files. Exit code 0 means all passed.- Use
deepStrictEqualfor objects, andthrowsorrejectsfor errors. - Reset shared data in
beforeEach. - Mock slow, costly or timed parts with
mock.fn,mock.methodandmock.timers. - Test servers on port 0, and measure coverage to find untested code.
Next: Debugging and Profiling. You will find bugs with the inspector and find slow and leaky code.