Skip to content

test_runner: support mocking non-existent modules - #66670

Closed
SahinurDEV wants to merge 1 commit into
nodejs:mainfrom
SahinurDEV:test-runner-mock-virtual-module
Closed

SahinurDEV wants to merge 1 commit into
nodejs:mainfrom
SahinurDEV:test-runner-mock-virtual-module

Conversation

@SahinurDEV

Copy link
Copy Markdown

Fixes: #55891
Refs: #62298

This picks up #62298 by @geeksilva97, which was auto-closed as stale after it had addressed the only review feedback. The approach and the virtual option name come from that PR, so the commit credits Edy Silva with Co-authored-by. It is rebased onto current main, where mock.module() has since gained options.exports and dual-package (cjsPath) handling, and it changes a few things described below.

Problem

t.mock.module('obsidian', { … }) throws ERR_MODULE_NOT_FOUND when obsidian isn't installed. Use cases from the issue: host-provided modules such as obsidian/electron that only exist at runtime, user code that a function wrapper loads, and optional peer dependencies.

Root cause

mock.module() first resolves the specifier with the real resolver (moduleLoader.resolveSync), so that it can key the mock by the module's canonical URL. Both interception points also resolve normally before they look up a mock: the loader resolve hook calls nextResolve(), and the CJS Module._load patch calls _resolveFilename(). A module that doesn't exist therefore fails in all three places before any mock lookup happens. This is by design. Mocks are keyed by resolved URL so that every specifier pointing at the same file gets the same mock (cjihrig: "no technical reason we couldn't do it"). There was just no key for something that doesn't resolve.

Fix

New option virtual (boolean, default false). It is opt-in, so mocking a missing module without it still throws, and typos keep failing loudly, as pmarchini asked for in the issue.

  • Registering (mock.js): if resolveSync throws ERR_MODULE_NOT_FOUND and virtual is true, the mock is keyed by getVirtualMockURL(specifier, callerURL) with format module:
    • bare specifier → node-test-mock:///<encodeURIComponent(specifier)>;
    • ./, ../, / or file: → the file URL it would resolve to, so ./x.js, ../dir/x.js and an absolute path to the same missing file share one mock.
  • ESM/hooks (loader.js resolve): only when nextResolve throws ERR_MODULE_NOT_FOUND, compute the same key from context.parentURL. If there is an active virtual mock under that key, resolve to it. After that, the existing versioning and caching path runs unchanged. load short-circuits virtual mocks without calling nextLoad, because there is nothing on disk.
  • CJS (Module._load patch): only when _resolveFilename throws MODULE_NOT_FOUND, compute the same key from parent.filename. If it names a virtual mock, use it. Otherwise the original error is rethrown.
  • Existing modules: if the specifier does resolve, virtual has no effect and the module is mocked through the normal path. This addresses @ljharb's review comment on test_runner: support mocking non-existent modules #62298: a mock must not shadow only some of the specifiers (pkg/foo, ./foo/index.js, ./foo, …) that reach a real module, because that would leave two sources of truth. Since the fallback only runs when resolution fails, a virtual mock can never take effect for a module that exists.
  • node: specifiers are never treated as virtual.

Changes compared with #62298:

Tests

New test/parallel/test-runner-module-mocking-virtual.js: validation of virtual; a missing module without virtual still throws; node: specifiers are rejected; a bare virtual mock works with import and require, default and named exports, and restore() brings back ERR_MODULE_NOT_FOUND/MODULE_NOT_FOUND; relative, absolute and file-URL paths to one missing file share a mock; virtual: true on an existing file intercepts both the path and the relative specifier; cache: true plus restore/re-mock.
Existing test/parallel/test-runner-module-mocking*.js pass. [fill in actual results]

Notes for reviewers

  • semver-minor (new option on an experimental API).
  • Once the PR number is known, add a changes: YAML entry for mock.module in doc/api/test.md (version: REPLACEME, this PR's URL, "Added virtual option.").

AI-assistance disclosure: I used an AI coding assistant to help investigate the issue and the earlier PR, and to draft the code, tests and this description. I reviewed, tested and understand all of the changes. The original approach is by @geeksilva97 (credited via Co-authored-by).

Add a `virtual` option to `mock.module()`. When it is `true` and the
specifier cannot be resolved, the mock is registered under a stable URL
instead of throwing `ERR_MODULE_NOT_FOUND`:

- bare specifiers map to a synthetic `node-test-mock:` URL;
- relative, absolute and file: URL specifiers map to the file: URL they
  would resolve to, so different paths to the same missing file share
  one mock.

The module loader's resolve hook and the CommonJS `Module._load` patch
only fall back to a virtual mock when normal resolution fails with a
"module not found" error, so existing modules are never shadowed. If
the module does exist, `virtual: true` has no effect and the module is
mocked as usual, so every path that resolves to it sees the mock.

Fixes: nodejs#55891
Refs: nodejs#62298
Co-authored-by: Edy Silva <edigleyssonsilva@gmail.com>
Signed-off-by: Sahinur <infosahinur@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/test_runner

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. test_runner Issues and PRs related to the test runner subsystem. labels Oct 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Welcome to Node.js, and thank you for your first contribution!

Before review, please take a moment to read:

Please make sure every commit is signed off. For a first pull request, GitHub Actions require collaborator approval and Jenkins CI must be started by a collaborator or triager, so an initial wait is normal.

@inoway46

Copy link
Copy Markdown
Contributor

As noted in the first-time contribution guidelines, we recommend focusing on one PR at a time until the first one is approved.
Could you close one of your two PRs for now?

@SahinurDEV

Copy link
Copy Markdown
Author

Thanks @inoway46, I missed that guideline. Closing this one for now so #66669 can go first; I'll reopen it once that one is approved. The branch stays on my fork.

@SahinurDEV SahinurDEV closed this Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run. test_runner Issues and PRs related to the test runner subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--experimental-test-module-mocks is not working as expected

3 participants