Repository navigation
test_runner: support mocking non-existent modules - #66670
Closed
SahinurDEV wants to merge 1 commit into
Closed
SahinurDEV wants to merge 1 commit into
SahinurDEV wants to merge 1 commit into
Conversation
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>
Collaborator
|
Review requested:
|
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. |
Contributor
|
As noted in the first-time contribution guidelines, we recommend focusing on one PR at a time until the first one is approved. |
Author
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.
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
virtualoption name come from that PR, so the commit credits Edy Silva withCo-authored-by. It is rebased onto currentmain, wheremock.module()has since gainedoptions.exportsand dual-package (cjsPath) handling, and it changes a few things described below.Problem
t.mock.module('obsidian', { … })throwsERR_MODULE_NOT_FOUNDwhenobsidianisn't installed. Use cases from the issue: host-provided modules such asobsidian/electronthat 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 loaderresolvehook callsnextResolve(), and the CJSModule._loadpatch 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, defaultfalse). 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.mock.js): ifresolveSyncthrowsERR_MODULE_NOT_FOUNDandvirtualis true, the mock is keyed bygetVirtualMockURL(specifier, callerURL)with formatmodule:node-test-mock:///<encodeURIComponent(specifier)>;./,../,/orfile:→ the file URL it would resolve to, so./x.js,../dir/x.jsand an absolute path to the same missing file share one mock.loader.jsresolve): only whennextResolvethrowsERR_MODULE_NOT_FOUND, compute the same key fromcontext.parentURL. If there is an active virtual mock under that key, resolve to it. After that, the existing versioning and caching path runs unchanged.loadshort-circuits virtual mocks without callingnextLoad, because there is nothing on disk.Module._loadpatch): only when_resolveFilenamethrowsMODULE_NOT_FOUND, compute the same key fromparent.filename. If it names a virtual mock, use it. Otherwise the original error is rethrown.virtualhas 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:
ERR_PACKAGE_PATH_NOT_EXPORTEDfor modules that do exist../ximported from two different directories would have collided.cache: truefollowed byrestore()cleans up correctly, and a later re-mock doesn't see stale exports.doc/api/test.md, which test_runner: support mocking non-existent modules #62298 did not have.Tests
New
test/parallel/test-runner-module-mocking-virtual.js: validation ofvirtual; a missing module withoutvirtualstill throws;node:specifiers are rejected; a bare virtual mock works withimportandrequire, default and named exports, andrestore()brings backERR_MODULE_NOT_FOUND/MODULE_NOT_FOUND; relative, absolute and file-URL paths to one missing file share a mock;virtual: trueon an existing file intercepts both the path and the relative specifier;cache: trueplus restore/re-mock.Existing
test/parallel/test-runner-module-mocking*.jspass. [fill in actual results]Notes for reviewers
changes:YAML entry formock.moduleindoc/api/test.md(version: REPLACEME, this PR's URL, "Addedvirtualoption.").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).