Skip to content

test_runner: add exports option to mock.module#61727

Open
Han5991 wants to merge 3 commits intonodejs:mainfrom
Han5991:test-runner-mock-exports
Open

test_runner: add exports option to mock.module#61727
Han5991 wants to merge 3 commits intonodejs:mainfrom
Han5991:test-runner-mock-exports

Conversation

@Han5991
Copy link
Contributor

@Han5991 Han5991 commented Feb 8, 2026

Summary

  • add mock.module(..., { exports }) and normalize option shapes through a shared exports path
  • keep defaultExport and namedExports as aliases, and emit runtime DeprecationWarning when legacy options are used
  • update docs, tests, and output fixtures/snapshots to reflect deprecation signaling and the new preferred option shape

Legacy Option Mixing Behavior

  • Mixed usage of options.exports with legacy options is currently allowed.
  • Values are normalized/merged into the same internal exports structure.
  • Legacy options are deprecated, but still supported for compatibility during migration.

Scope

Testing

  • make lint-js
  • python3 tools/test.py test/parallel/test-runner-module-mocking.js
  • python3 tools/test.py test/test-runner/test-output-coverage-with-mock.mjs test/test-runner/test-output-typescript-coverage.mjs

Refs: #58443

@nodejs-github-bot
Copy link
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 Feb 8, 2026
@Han5991 Han5991 marked this pull request as draft February 8, 2026 00:27
@Han5991 Han5991 force-pushed the test-runner-mock-exports branch from 3d95c0c to fa99ada Compare February 8, 2026 00:35
@JakobJingleheimer
Copy link
Member

JakobJingleheimer commented Feb 8, 2026

Related: #58443

(I haven't read through this PR yet though)

@Han5991
Copy link
Contributor Author

Han5991 commented Feb 8, 2026

Related: #58443

(I haven't read through this PR yet though)

@JakobJingleheimer

I was working on it with that in mind.
I'm planning to proceed only up to Step 2 of the plan. Is that okay?

@Han5991 Han5991 force-pushed the test-runner-mock-exports branch from 73c4cf1 to 715c12c Compare February 8, 2026 01:11
@JakobJingleheimer
Copy link
Member

JakobJingleheimer commented Feb 8, 2026

Yes, sounds good go me!

It would be ideal (but not required or anything) to land this with a userland migration ready to go. I think it could be done fairly easily.

Cc @Ceres6 this should make your Jest β†’ node:test migration easier πŸ™‚

@Han5991
Copy link
Contributor Author

Han5991 commented Feb 8, 2026

Thanks for the feedback. I will scope this PR to Step 2 of the plan: introduce options.exports, keep defaultExport/namedExports as aliases (with deprecation signaling), and defer userland migration + removal to follow-up work.

@Han5991 Han5991 marked this pull request as ready for review February 8, 2026 01:33
@codecov
Copy link

codecov bot commented Feb 8, 2026

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 89.72%. Comparing base (0777dcc) to head (13b13b8).
⚠️ Report is 124 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #61727      +/-   ##
==========================================
- Coverage   89.73%   89.72%   -0.01%     
==========================================
  Files         675      676       +1     
  Lines      204525   206034    +1509     
  Branches    39310    39499     +189     
==========================================
+ Hits       183529   184865    +1336     
- Misses      13287    13302      +15     
- Partials     7709     7867     +158     
Files with missing lines Coverage Ξ”
lib/internal/test_runner/mock/loader.js 99.35% <100.00%> (ΓΈ)
lib/internal/test_runner/mock/mock.js 98.78% <100.00%> (+0.05%) ⬆️

... and 182 files with indirect coverage changes

πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • πŸ“¦ JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Han5991 Han5991 force-pushed the test-runner-mock-exports branch from 715c12c to 424675e Compare February 8, 2026 02:47
Add options.exports support in mock.module() and normalize option

shapes through a shared exports path.

Keep defaultExport and namedExports as aliases, emit runtime

deprecation warnings for legacy options, and update docs and tests,

including output fixtures and coverage snapshots.

Refs: nodejs#58443
@Han5991 Han5991 force-pushed the test-runner-mock-exports branch from 424675e to 0498de3 Compare February 8, 2026 07:24
Copy link
Member

@JakobJingleheimer JakobJingleheimer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking this up!

Comment on lines +826 to +829
if ('exports' in options) {
validateObject(options.exports, 'options.exports');
copyOwnProperties(options.exports, moduleExports);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since last-wins, I think the "new" should win (and be moved below namedExports and defaultExport copying).

Or maybe when exports exists, the others should be ignored? I feel like there's some kind of back-version compatibility thing here. @ljharb thoughts?

Comment on lines 834 to 839
copyOwnProperties(options.namedExports, moduleExports);
}

if ('defaultExport' in options) {
emitLegacyMockOptionWarning('defaultExport');
moduleExports.default = options.defaultExport;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namedExports copies the descriptors, but defaultExport does direct assignment. Why the difference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is intentional β€” namedExports copies descriptors of the properties of the provided object (so getters/setters are preserved per export slot), while defaultExport receives the value itself, not an
object whose property structure matters. Copying ObjectGetOwnPropertyDescriptor(options, 'defaultExport') is semantically equivalent to direct assignment in practice. That said, if you'd prefer we align the two
for visual consistency, happy to apply ObjectDefineProperty for defaultExport as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's only true for the original module. MockModuleOptions.defaultExport could be a getter:

mock.module('…', {
  get defaultExport() { return mock.fn() },
});

But I see you already made the switch to ObjectDefineProperty + ObjectGetOwnPropertyDescriptor.

Could you please add a test-case to cover this scenario so nobody comes along in future and unwittingly breaks that?

Rename the named export in the mock.module() example from `fn`
to `foo` for improved readability.
- Unify internal export representation into a single `moduleExports`
  object, removing the split into `defaultExport`, `hasDefaultExport`,
  and `namedExports`.
- DRY up `emitLegacyMockOptionWarning` with a lookup map instead of a
  switch statement with separate flag variables.
- Use `ObjectDefineProperty` for `defaultExport` option to be
  consistent with descriptor copying in `namedExports`.
Copy link
Member

@JakobJingleheimer JakobJingleheimer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is looking pretty good :)

Comment on lines 834 to 839
copyOwnProperties(options.namedExports, moduleExports);
}

if ('defaultExport' in options) {
emitLegacyMockOptionWarning('defaultExport');
moduleExports.default = options.defaultExport;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's only true for the original module. MockModuleOptions.defaultExport could be a getter:

mock.module('…', {
  get defaultExport() { return mock.fn() },
});

But I see you already made the switch to ObjectDefineProperty + ObjectGetOwnPropertyDescriptor.

Could you please add a test-case to cover this scenario so nobody comes along in future and unwittingly breaks that?

Comment on lines +2448 to +2449
This option is deprecated and will be removed in a future major release.
Prefer `options.exports.default`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to wait for a future major release? the feature is experimental and may not follow Semver?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. This could probably be more vague, like "a later version".

Comment on lines +846 to +854
function emitLegacyMockOptionWarning(option) {
if (warnedLegacyOptions[option]) return;
warnedLegacyOptions[option] = true;
process.emitWarning(
`mock.module(): options.${option} is deprecated. ` +
`Use ${kLegacyOptionReplacements[option]} instead.`,
'DeprecationWarning',
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lib/internal/util provides a deprecateProperty utility. Can't we use that?

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.

5 participants