Skip to content
Node.js nd testing 5 min read

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 typeScopeSpeedDependenciesUse when
UnitOne function/moduleVery fastMockedVerifying logic and edge cases
IntegrationA few modules + real depsMediumReal (DB, HTTP)Verifying wiring between parts
End-to-endWhole appSlowFull stackVerifying 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.

RunnerInstallStyleNotable for
node:testBuilt in (Node 20+)describe/it + node:assertZero dependencies, native ESM
Jestnpm i -D jestdescribe/it/expectBatteries-included, big ecosystem
Vitestnpm i -D vitestJest-compatible APIFast, Vite-native, great TS/ESM
Mochanpm i -D mochadescribe/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:test runner 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 test the 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.
Last updated June 14, 2026
Was this helpful?