feat(ext/node): implement node:test mock.module#35329
Merged
Merged
Conversation
Adds `mock.module(specifier, options)` to the node:test `mock` object,
matching Node 26.3.0. It replaces the module resolved from `specifier`
with a synthetic module exposing `defaultExport` and `namedExports` for
the duration of the test, and returns a `MockModuleContext` with
`.restore()`.
The implementation is built on top of `module.registerHooks()` rather
than a parallel mechanism. On the first `mock.module()` call it lazily
registers a single resolve/load hook pair that intercepts both ESM
`import()` and CJS `require()`:
- The load hook serves synthetic source for any resolved url that maps
to an active mock. CommonJS importers get `module.exports` with named
exports applied onto the default export; ESM importers get an
independent default and named exports.
- The resolve hook versions the resolved url for ESM importers so that
the `cache: false` default yields a fresh module on every import,
while `cache: true` reuses a stable url. CJS cache behavior is handled
by evicting (and on restore, restoring) the require cache entry.
The live `namedExports` / `defaultExport` values cross into the
synthetic module via a registry published on `globalThis` under a
well-known symbol; the generated source reads them back by url key.
Everything is primordial-only and lazily loaded, so importing
`node:test` without mocking modules registers no hooks and adds no
startup cost.
Spec tests under tests/specs/node/node_test_mock_module cover named +
default exports, ESM and CJS importers, the cache option, restore(), and
interaction with mock.reset()/restoreAll().
JSON mocking and `with { type: "json" }` import-attribute handling are
deferred to the in-flight import-attributes work (#35028).
- detectFormat used an undefined getUrlHelpers(), which threw and was
swallowed by the catch, silently detecting every .js file URL as
commonjs. Use getNodeUrl().fileURLToPath() instead, and treat
http(s) urls as ESM.
- ESM source generation emitted `export let <name>` with the raw export
name, producing a syntax error for non-identifier names. Use a safe
local identifier plus a string export name.
- Add a js_format spec test covering .js ESM detection and a
non-identifier ("foo-bar") named export.
bartlomieju
marked this pull request as ready for review
June 20, 2026 09:44
Adds the legacy `exports` option to mock.module(): its `default` key becomes the default export and its remaining own enumerable keys become named exports. It cannot be combined with `namedExports` or `defaultExport` (ERR_INVALID_ARG_VALUE), and must be an object (ERR_INVALID_ARG_TYPE), matching Node. Spec tests cover the three validations and the exports semantics for ESM and CJS modules observed from both module systems.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
mock.module(specifier, options)on thenode:testmock object,matching Node 26.3.0. It replaces the module resolved from
specifierwith asynthetic module exposing
defaultExportandnamedExportsfor the durationof the test, working for both ESM
import()and CJSrequire(), and returns aMockModuleContextwith.restore()that also participates inmock.reset()/mock.restoreAll(). The legacyexportsoption is alsosupported: its
defaultkey becomes the default export and its remaining ownenumerable keys become named exports; it cannot be combined with
namedExportsor
defaultExport.The feature is built on top of
module.registerHooks()(the modulecustomization hooks landed in #35026 / #35027) rather than a parallel
mechanism. The first
mock.module()call lazily registers a singleresolve/load hook pair that intercepts both module systems:
active mock. For a CommonJS module the named exports are applied onto the
default export (throwing the same "Cannot create mock..." error Node throws
when they cannot be); for an ESM module the default and named exports are
exposed independently.
default
cache: falseyields a fresh module namespace on every import whilecache: truereuses a stable url. The CJS side is handled by evicting therequire cache on setup and restoring the original cached instance on
restore().The live
namedExports/defaultExportvalues cross into the syntheticmodule through a registry published on
globalThisunder a well-known symbol;the generated source reads them back by url key, so functions and objects pass
through unchanged.
The whole subsystem is primordial-only and lazily initialized: importing
node:testwithout mocking modules registers no hooks and adds no startupcost, in line with the zero-cost-when-unused direction for the mock subsystem.
Spec tests under
tests/specs/node/node_test_mock_modulecover named + defaultexports, the ESM importer, the CJS require importer, a
.jsmodule (ESM formatdetection), non-identifier export names, the
exportsoption and itsvalidations, the cache option, restore, and the interaction with
mock.reset()/mock.restoreAll().The upstream Node conformance file
parallel/test-runner-module-mocking.jsisintentionally not enabled in
tests/node_compat/config.jsoncyet. Thenode_compat runner judges a test file by its overall exit code, and that file
cannot pass as a whole in Deno for two independent reasons:
with { type: "json" }import attributes (fix(ext/node): import attributes and custom module types in registerHooks ESM hooks #35028), automatic per-test mockrestore (Deno's
node:testhas no per-test mock tracker yet), and livegetter evaluation of
defaultExport/exports.default(this PR snapshotsexport values eagerly).
process.execPathwith the Node-only flag--experimental-test-module-mocks, which Deno does not accept, so they failregardless of feature completeness.
The implemented behavior is gated by the spec tests above; the conformance file
can be enabled once the deferred lifecycle / import-attribute work lands.
Deferred, and called out so they are not mistaken for regressions:
with { type: "json" }import attributes depend on thein-flight import-attributes / custom-module-type work (fix(ext/node): import attributes and custom module types in registerHooks ESM hooks #35028).
finishes; Deno's
node:testhas no per-test mock tracker yet, so theconformance cases that rely on automatic per-test restore still need that
separate lifecycle feature. Explicit
restore()/reset()/restoreAll()all work.defaultExport/exports.defaultgetters are read once at mock setuprather than per module-system evaluation; live getter semantics need the
registry to read through to the original options object.