1Step-by-Step Breakdown
A Test Runner Ships in Core. Since Node 18 (stable in Node 20), the node:test module provides a complete test runner built into the runtime itself ā describe/test blocks, assertions via node:assert, mocking, and coverage reporting ā all without installing Jest, Mocha, or any other framework. For small services and libraries, this can eliminate an entire category of dependency and configuration overhead.
Running Tests: node --test. The runner is invoked with node --test, which by default recursively discovers any file matching common test naming conventions (*.test.js, *.test.mjs, files inside a test/ directory, etc.) and executes them, printing TAP-compatible output that most CI dashboards already know how to parse.
describe/test and Nested Suites. Just like Jest or Mocha, node:test supports describe() blocks for grouping related tests and nesting suites hierarchically, along with beforeEach/afterEach/before/after lifecycle hooks for setup and teardown ā the API surface will feel immediately familiar to anyone coming from an established framework.
Built-in Mocking with t.mock. Each test receives a t (TestContext) argument exposing t.mock, a built-in mocking utility that can spy on functions, replace methods, and track call counts ā covering a large share of what Sinon.js is traditionally reached for, without adding it as a dependency.
Code Coverage Without nyc/Istanbul. Passing --experimental-test-coverage (stabilizing across recent versions) generates a code coverage report directly from V8's own instrumentation, without configuring nyc or Istanbul separately ā reporting line, branch, and function coverage right in the terminal or as an LCOV file for CI upload.
Watch Mode for TDD Workflows. The --watch flag re-runs affected tests automatically whenever a source or test file changes, enabling a tight red-green-refactor TDD loop without a third-party watcher like nodemon or Jest's own watch mode wired into the file system.
When Jest/Mocha Still Make Sense. The native runner deliberately keeps its feature set lean: it lacks Jest's snapshot testing, its rich matcher ecosystem (toMatchObject, toHaveBeenCalledWith), and some of Mocha's plugin ecosystem for specialized reporters. For a large existing test suite already invested in those features, migrating away is rarely worth it ā but for new services and libraries, starting with node:test avoids locking in a dependency you may not need.
You want to spy on a function call and assert it was called exactly once, without adding Sinon.js as a dependency. Which built-in node:test feature covers this?
- āt.mock ā the TestContext's built-in mocking utility
- ānode:assert alone, with no mocking support
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1A Lean Test Suite Ships Fixes for Accessibility Regressions Faster
Removing framework installation and configuration friction lowers the barrier to writing a test at all ā including accessibility-adjacent regression tests (e.g. verifying an API always returns the ARIA-relevant fields a frontend needs). Teams are more likely to add a quick test when the tooling overhead is near zero.
SEO Implications
- 1
Faster CI From a Dependency-Free Test Runner Speeds Up Deploy Cadence
Removing Jest or Mocha and their transitive dependencies from CI install steps reduces pipeline time, enabling faster iteration and quicker fixes to any production issue ā including SEO-relevant regressions like broken structured data or slow server-rendered responses.
Best Practices
Default to node:test for new, focused services and libraries
It ships with the runtime, requires zero configuration, and covers assertions, mocking, and coverage ā for a greenfield project, this avoids locking in a framework dependency before you know you need its extra features.
Standardize on the *.test.js naming convention so tests are auto-discovered
node --test relies on file naming/location conventions for discovery by default; deviating from them means tests silently never run, which is far more dangerous than a test that visibly fails.
Frequent Bugs
Running node --test reports "tests 0, pass 0, fail 0" even though test files clearly exist in the repository.
This means the runner's file-discovery pattern didn't match any files ā check that test files are named with a recognized suffix (like .test.js) or located under a test/ directory, since node --test does not execute arbitrary .js files by default.
Real-World Examples
Removing Mocha, Chai, and Sinon From a Small Internal Service
A small internal notification service depended on Mocha, Chai, and Sinon purely for unit testing a handful of pure functions and one Express route. Migrating to node:test replaced all three dependencies with zero new installs, cut CI install time noticeably, and kept the same assertion style using node:assert's strictEqual/deepStrictEqual, which map closely to Chai's common matchers.
// Before: 3 dev dependencies
// "mocha", "chai", "sinon"
// After: 0 new dependencies
import { test } from "node:test";
import assert from "node:assert";