Skip to content

Testing & Tooling

Writing code is only half the job. Knowing how to test, debug, measure, and configure your Node.js environment turns a working script into a maintainable, production-ready application.

Node 24 (the active LTS) ships a built-in test runner, type stripping for .ts files, a powerful inspector protocol, and perf_hooks for timing — meaning you can do a lot without installing anything.

LineStatus (Sep 2026)Use it for
Node 22Maintenance LTS (until Apr 2027)Existing production apps — require(esm), type stripping since 22.18
Node 24Active LTS (LTS since Oct 2025)New production apps — this course targets it
Node 26Current (LTS from Oct 2026)Trying Temporal, Iterator.concat(), Undici 8

Node 18 and 20 are end-of-life. When a lesson says “Node 22+”, the feature exists on every supported line.

LessonTopic
Built-in Test Runnernode:test, node:assert, test(), describe/it, node --test
Debuggingnode --inspect, Chrome DevTools, breakpoints, console methods
Performanceperf_hooks, performance.now(), PerformanceObserver, avoiding blocking
Ecosystem & ToolchainTypeScript runners, ESLint/Prettier/Biome, nodemon, env files
flowchart TB
  subgraph Project["Your Node.js Project"]
    direction TB
    Source["Source code (.ts / .js / .mjs)"]
    Lint["Linter / Formatter"]
    TSR["TypeScript runner"]
    TestRunner["Test runner"]
    Debugger["Debugger"]
    Perf["Performance"]
    Source --> Lint --> TSR --> TestRunner --> Debugger --> Perf
  end
  Lint --> LintTools["ESLint, Prettier, Biome"]
  TSR --> TSRTools["node file.ts (built-in type stripping); tsx for enum etc."]
  TestRunner --> TestTools["node:test (built-in) or Vitest / Jest"]
  Debugger --> DebuggerTools["node --inspect + Chrome DevTools"]
  Perf --> PerfTools["perf_hooks, PerformanceObserver"]
A Node.js project's toolchain: source flows through the linter, TypeScript runner, test runner, debugger, and performance tools

Node’s built-ins cover most basic needs. Here is a quick decision guide:

Terminal window
# Zero dependencies — use the built-in runner
node --test src/**/*.test.js
# TypeScript without a build step (Node 22.18+ / 24 — type stripping is on by default)
node src/app.ts
# Run a package.json script without npm (Node 22+)
node --run dev
# Faster HMR + watch mode
npx tsx watch src/app.ts
# Lint + format in one tool (zero config)
npx biome check --apply src/

What’s already available, with zero installs

Section titled “What’s already available, with zero installs”

Every capability this module covers is already inside the node binary you have. A quick self-check, run for real on Node 24:

Node.js
Output (verified on Node 24)
node:test available: true
perf_hooks available: true
global fetch available: true
type stripping (.ts files) is on by default on Node 24 -- no flag needed

Each lesson in this module answers one question about the same running process — think of it as four lenses pointed at the same node binary, not four separate tools:

flowchart TB
  Q1["Is my code correct?"] -->|"node:test + node:assert"| L1["Built-in Test Runner"]
  Q2["Why did it do that?"] -->|"--inspect, console.*, uncaughtException"| L2["Debugging"]
  Q3["Is it fast enough?"] -->|"perf_hooks, --cpu-prof, worker_threads"| L3["Performance"]
  Q4["How do I run/ship it?"] -->|"--env-file, --watch, node --run, Docker"| L4["Ecosystem & Toolchain"]
  L1 --> Ship["Code you can trust in production"]
  L2 --> Ship
  L3 --> Ship
  L4 --> Ship
Four questions about the same process: correct, debuggable, fast enough, shippable
ChoiceBenefitCost
Built-in node:test / perf_hooks / --inspectZero install, zero config drift, works the same on every machine that has NodeFewer conveniences than a mature ecosystem tool (no coverage UI, no watch-mode test filtering out of the box)
Third-party (Vitest, ESLint, pino, tsx)Richer DX, plugin ecosystems, features Node hasn’t stabilized yetA dependency to install, pin, audit, and keep updated — and a lockfile everyone must trust
Learning the built-ins firstDebugging AI-generated or unfamiliar code never depends on “which test framework does this repo use”Slightly more typing for things a framework would give you as sugar (e.g. expect().toBe() vs assert.equal())
  • “You need Jest or Mocha to write real tests in Node” — fact: node:test has been built in since Node 18, and by Node 24 it covers mocking, fake timers, snapshots, and coverage without a single dependency.
  • “TypeScript needs a build step to run on Node” — fact: Node 24 strips type annotations at load time by default; you only need tsx/ts-node/tsc for syntax that needs to be transformed (enum, decorators) rather than erased.
  • “perf_hooks / node —inspect / node:test are experimental or unstable” — fact: all three are stable APIs; only specific sub-features (coverage, some test-runner extras) still carry an --experimental- flag, and each lesson calls those out explicitly.
  • Does a snippet reach for jest/mocha/chai as a dependency for a task node:test + node:assert already cover? That’s an unnecessary dependency, not a correctness bug — but it’s a signal the generated code wasn’t written with Node 24 in mind.
  • Does it add ts-node or a bundler step for a plain .ts file with only type annotations (no enum, no decorators)? node file.ts already works.
  • Does it call a Node built-in behind an --experimental- flag (test coverage, module mocks) without mentioning the flag or the warning it prints? Running the snippet as shown will crash or silently no-op.

🐛 Spot the bug

import { test } from 'node:test';
import assert from 'node:assert/strict';
// package.json: "test": "node --test"
test('creates a user', async function() {
const user = await createUser({ name: 'Ada' });
assert.equal(user.name, 'Ada');
});
test.only('debugging this one locally', function() {
assert.ok(true);
});
Show the answer

test.only marks exactly one test to run exclusively — but only when the runner is invoked with --test-only (or run({ only: true })). The package.json script above is plain node --test, so without --test-only both tests still run normally, .only is a no-op, and nothing looks wrong locally. The real danger is the opposite setup: a CI script that always passes --test-only. There, forgetting to remove test.only after a debugging session silently excludes every other test in the file from ever running again — and CI stays green while regressions in the skipped tests go uncaught. Search test files for .only before every commit, or lint for it.

Which Node.js version introduced the built-in node:test runner?
Which built-in module is used for measuring execution time in Node.js?
How do you run a TypeScript file directly on Node 24 without a build step?
What does this print on Node 24? import { test } from 'node:test'; console.log(typeof test);
A CI test script is node --test --test-only. A developer leaves test.only(...) on one test after debugging and pushes it. What happens?
Why does this module teach node:test / --inspect / perf_hooks before mentioning any third-party tool?