# Testing with the Built-in Test Runner

Course: [Node.js](https://www.devtutorials.ai/learn/nodejs) · Section: Quality and Production
Source: https://www.devtutorials.ai/learn/nodejs/quality-production/testing

> 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 from `node: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:

1. Why tests matter, and the Arrange-Act-Assert pattern
2. `test()`, `describe()` and `it()`, and running `node --test`
3. Subtests, async tests, `skip`, `todo`, `only` and timeouts
4. Assertions with `node:assert/strict`
5. Hooks: `before`, `after`, `beforeEach`, `afterEach`
6. Mocks for functions, methods and timers
7. Testing files, events, streams, HTTP servers and CLI tools
8. Coverage, watch mode and isolation
9. `node:test` compared 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:
  1. **Arrange** = prepare the data.
  2. **Act** = call the code.
  3. **Assert** = check the result.

**Simple lesson:** prepare, call, check. Every good test follows these three steps.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 170" role="img" ariaLabelledby="fig-node-redgreen" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-redgreen">Animation: the test cycle. Write a failing test, make it pass, then clean up the code</title>
<rect x="20" y="55" width="150" height="60" rx="12"  style={{ fill: "var(--ngc-red-soft)", stroke: "var(--ngc-red)", strokeWidth: 1.5 }}/><text x="95" y="82" textAnchor="middle" style={{ fill: "var(--ngc-red)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>1. Red</text><text x="95" y="100" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>the test fails</text>
<rect x="205" y="55" width="150" height="60" rx="12"  style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }}/><text x="280" y="82" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>2. Green</text><text x="280" y="100" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>fix code, test passes</text>
<rect x="390" y="55" width="150" height="60" rx="12"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }}/><text x="465" y="82" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>3. Clean up</text><text x="465" y="100" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>tests still pass</text>
<path d="M170 85 L205 85 M355 85 L390 85"  style={{ stroke: "var(--ink)", strokeWidth: 1.5 }}/>
<path d="M465 115 Q465 150 280 150 Q95 150 95 115"  style={{ fill: "none", stroke: "var(--ink)", strokeDasharray: "4 4" }}/>
<rect x="18" y="53" width="154" height="64" rx="13" style={{ fill: "none", stroke: "var(--ngc-orange)", strokeWidth: 3 }}><animate attributeName="x" values="18;18;203;203;388;388" keyTimes="0;0.3;0.33;0.63;0.66;1" dur="6s" repeatCount="indefinite" /></rect>
<text x="280" y="30" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>node --test runs every *.test file and reports each result</text>
</svg>
<figcaption>Red, green, clean up. Then repeat for the next small feature.</figcaption>
</figure>

## node:test Basics

- **`test(name, fn)`** = one test. It passes if `fn` finishes without throwing.
- **`describe(name, fn)`** = a group of tests. **`it`** = another name for `test`, 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.js` and every file in a `test/` 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`. Change `0.125` to `0.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, with `node --test --test-only`.
- **Timeouts** = `{ timeout: 1000 }` fails a test that takes longer than 1 second.
- **Reporters** = the output style. `--test-reporter=spec` is the readable tree. `tap` and `dot` also 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 in `fn.mock.calls`.
- **`mock.method(obj, "name", impl)`** = replace one method on an object.
- **`mock.timers`** = fake time. `enable()`, then `tick(ms)` moves time forward at once.
- **Restore** = `m.mock.restore()` or `mock.restoreAll()` puts the real thing back. Inside a test, `t.mock` restores 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 with `fetch`, then `close()` the server.
- **CLI tools** = run them with `execFile` and check stdout and the exit code.

Testing a CLI tool runs a real child process. Here it is with real Node.js:

```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:

```text
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-coverage` prints a table.
- **Watch mode** = `node --test --watch` runs 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=1` runs them one at a time.

Add `"test": "node --test"` to `package.json`, so `npm test` runs your tests.

`grades.js`

```js
export function grade(mark) {
  if (mark >= 90) return "A";
  if (mark >= 35) return "Pass";
  return "Fail";
}
```

`grades.test.js`

```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`

```json
{ "type": "module" }
```

```text
$ 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

<div className="tscroll">

| 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 |

</div>

## TL;DR

1. **Test** = code that checks code. **Arrange, Act, Assert.**
2. **`test`, `describe`, `it`** from `node:test`. Run with **`node --test`**.
3. **Exit code** = 0 all pass, 1 any fail.
4. **`skip`**, **`todo`**, **`only`**, **`timeout`** = control which tests run.
5. **Assertions** = `equal`, `deepStrictEqual`, `throws`, `rejects`, `match`.
6. **Hooks** = `before`, `after`, `beforeEach`, `afterEach`.
7. **Mocks** = `mock.fn`, `mock.method`, `mock.timers`.
8. **Node.js code** = temp folders, `once`, `toArray`, port 0, `execFile`.
9. **Coverage** = `--experimental-test-coverage`. **Watch** = `--watch`.

## Check Yourself

## Exercises

## What to Remember

- A test arranges data, acts, and asserts the result.
- `node --test` finds and runs test files. Exit code 0 means all passed.
- Use `deepStrictEqual` for objects, and `throws` or `rejects` for errors.
- Reset shared data in `beforeEach`.
- Mock slow, costly or timed parts with `mock.fn`, `mock.method` and `mock.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.
