Introduction to Testing in Node.js
Automated tests are the safety net that lets you change Node.js code with confidence — they catch regressions before users do, document how your code is meant to behave, and turn “I think this works” into “I can prove it works.” The Node ecosystem has matured to the point where you no longer need a third-party library just to start: modern Node ships a capable test runner in core. This page maps out the kinds of tests you’ll write, how the test pyramid helps you balance them, and the runners you’ll choose between so the rest of this section makes sense.
Types of tests
Tests are usually grouped by how much of the system they exercise. The three you’ll meet most often are unit, integration, and end-to-end (e2e), and each trades speed for realism.
Unit tests verify a single function or module in isolation, with its collaborators replaced by fakes. They are fast (milliseconds), deterministic, and pinpoint exactly what broke. A pure function that formats a price or validates an email is the ideal unit-test target.
Integration tests exercise several units working together — typically your code plus a real dependency such as a database, an in-memory queue, or an HTTP server. They are slower and a little flakier, but they catch the wiring mistakes unit tests can’t see, like a wrong SQL column name or a misconfigured route.
End-to-end tests drive the whole application the way a real client would: spin up the server, send real HTTP requests, and assert on the responses (or, for a UI, click through a real browser). They give the most confidence per test but are the slowest and most brittle.
| Test type | Scope | Speed | Dependencies | Use when |
|---|---|---|---|---|
| Unit | One function/module | Very fast | Mocked | Verifying logic and edge cases |
| Integration | A few modules + real deps | Medium | Real (DB, HTTP) | Verifying wiring between parts |
| End-to-end | Whole app | Slow | Full stack | Verifying critical user flows |
The test pyramid
The test pyramid is a guideline for how to distribute effort across those types. The idea is simple: write many cheap, fast unit tests at the base, fewer integration tests in the middle, and only a handful of slow e2e tests at the top covering your most important flows.
/\ e2e (few) slow, high confidence
/ \
/----\ integration (some)
/ \
/--------\ unit (many) fast, cheap
Inverting the pyramid — leaning on a huge suite of e2e tests — produces a slow, flaky build that developers stop trusting. A healthy suite runs in seconds locally, so you actually run it on every change.
The pyramid is a heuristic, not a law. For an I/O-heavy API with thin business logic, a “trophy” shape (more integration tests) often gives better confidence than chasing 100% unit coverage of trivial glue code.
The Node.js testing landscape
For years you needed an external framework to test Node. Today you have a strong built-in option plus several mature libraries. Here’s how they compare.
| Runner | Install | Style | Notable for |
|---|---|---|---|
node:test | Built in (Node 20+) | describe/it + node:assert | Zero dependencies, native ESM |
| Jest | npm i -D jest | describe/it/expect | Batteries-included, big ecosystem |
| Vitest | npm i -D vitest | Jest-compatible API | Fast, Vite-native, great TS/ESM |
| Mocha | npm i -D mocha | describe/it (BYO assert) | Flexible, long-established |
The built-in test runner (the node:test module) needs no dependencies and runs files directly. It uses the node:assert module for assertions and supports subtests, mocking, and coverage out of the box.
import { test } from 'node:test';
import assert from 'node:assert/strict';
function add(a, b) {
return a + b;
}
test('add() sums two numbers', () => {
assert.equal(add(2, 3), 5);
});
Run it directly with Node — the --test flag discovers and executes your test files.
node --test
Output:
✔ add() sums two numbers (0.9ms)
ℹ tests 1
ℹ pass 1
ℹ fail 0
Jest is the most popular all-in-one framework: it bundles a runner, an expect assertion library, mocking, snapshots, and coverage. Vitest offers a nearly identical API but is dramatically faster and treats ESM and TypeScript as first-class, making it the modern default for new Vite-based projects. Mocha is the veteran — flexible and unopinionated, but you assemble your own assertions (Chai) and mocks (Sinon).
A typical package.json wires a runner to the test script so npm test just works:
{
"scripts": {
"test": "node --test",
"test:watch": "node --test --watch"
}
}
What this section covers
The pages that follow take you from your first test to a fully tested API. You’ll start with the built-in runner, see how Jest and Vitest compare in practice, learn to isolate code with mocks and stubs, handle the async nature of Node correctly, and finish by testing real HTTP endpoints. Pick the runner that fits your project — the concepts (arrange-act-assert, the pyramid, mocking boundaries) carry across all of them.
Best Practices
- Lean on the pyramid: write mostly fast unit tests, fewer integration tests, and a small set of e2e tests for critical paths.
- Keep unit tests deterministic — no real network, clock, or filesystem; replace those collaborators with mocks.
- Start with the built-in
node:testrunner for new projects; reach for Jest or Vitest only when you need their extra features. - Name tests by behavior (“returns 404 for unknown user”), not by implementation, so they survive refactors.
- Make
npm testthe single command that runs everything, and run it in CI on every push. - Use integration tests against a real (often containerized or in-memory) dependency to catch wiring bugs unit tests miss.
- Optimize for a suite that runs in seconds locally, so developers actually run it before committing.