Skip to content

Build1 publisher2 min readPublished

A bare test() in node --test cancels the subtest you did not await

Node's built-in runner now covers mocking, fake timers and TypeScript execution, so a backend library can skip the framework install. Adopting it means learning where test() and describe() disagree, and living with three experimental flags.

The Engineer · Build desk

What happened

  • Node has shipped a test runner since v18 and marked it stable in v20; the 2026 version adds mocking, fake timers, watch mode, coverage output, global setup hooks and direct execution of .ts files.
  • Run node --test with no arguments and it walks the tree for a fixed pattern set, including anything under a test/ directory and .ts, .cts and .mts files unless --no-strip-types is passed.
  • Node strips the type annotations and runs the result, so a project still needs tsc --noEmit in CI if type errors are going to fail the build.
  • A subtest created inside a bare test() is not awaited by its parent, so the parent finishes first and the outstanding subtest is cancelled and marked as a failure.
  • mock.fn() returns a spy carrying call metadata, and t.mock.method() patches a method on an object and restores it when the test ends.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Anyone adding Jest or Vitest to a package with no browser code and no JSX now has to name the feature that pays for the config file and the transitive tree.
  • cost Porting an existing Jest suite costs a read of every nested test() call, because nesting that always worked there produces a cancelled subtest reported as a failing one here.
  • capability A small typed library can now ship its tests with no test devDependencies at all, since the runner executes .ts files without ts-node, tsx or a build step.

`describe` and `it` are alias pairs for `suite` and `test` [6]. Inside a `describe`, sibling tests are enqueued together and the parent does not need an `await` [7]. If you write in the suite style, the cancellation case never arises. For a suite that currently nests freely, that is most of the migration work.

Each test file runs in its own child process by default, so you get isolation between files with no config, and the programmatic `run()` API exposes the same setting as `isolation: 'process' | 'none'` [8]. The article gives no timings [18], so a team swapping runners to shorten CI has nothing here to compare against.

Fake timers are opt-in per API: `t.mock.timers.enable({ apis: ["setTimeout"] })` turns on the setTimeout fake, and `t.mock.timers.tick(1000)` advances it inside the test [10]. The import shape in your production code decides whether that works at all. A destructured `import { setTimeout } from 'node:timers'` is not mockable; reference the timer off the global or the module namespace and the fake applies [11].

Three of the six capabilities in the pitch still carry a label [17]. Coverage runs behind `--experimental-test-coverage` [12], `--watch` is experimental [13], and `--test-global-setup`, which landed in v24 and points at a module exporting `globalSetup` and `globalTeardown`, is marked early development [14]. The lcov reporter feeds Codecov or SonarQube and emits no human-readable results, so a CI job that wants both a coverage file and a readable log runs two reporters [12].

For the swap to hold on your package, the conditions are the ones the source names: no browser code, no JSX, no transform pipeline worth speaking of [16]. JSDOM, browser mode, a snapshot ecosystem and a plugin API are all missing, so any of those in your current suite is the dependency's justification [15]. Type checking has to happen elsewhere, because Node strips annotations without checking them [4]. For React components, the article's advice is to keep Vitest [15].

What to watch

  • Whether --experimental-test-coverage loses its flag in a Node release, since that is what a required coverage check in CI depends on.
  • Whether --test-global-setup keeps the globalSetup/globalTeardown export shape as it moves off the early-development label.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories