Skip to content

mock.module: consolidate defaultExport + namedExports into exports #58443

Description

@JakobJingleheimer

Currently, mock.module's options expects defaultExport and namedExports separately. We are not aware of a reason to separate them, and it's inconsistent with other major testing frameworks/utilities (such as Jest).

function mock__default() {…}
function mock__foo() {…}

mock.module('example.mjs', {
-  defaultExport: mock__default,
-  namedExports: {
+  exports: {
+    default: mock__default,
     foo: mock__foo,
   },
});

The plan would be:

  1. Immediately introduce options.exports
    • alias defaultExport & namedExports into options.exports
  2. Immediately flag defaultExport & namedExports deprecated
  3. Provide a userland-migration to automatically transform old to new
  4. In node 25.x remove defaultExport & namedExports

Activity

  1. changed the title [-]mocks: consolidate `defaultExport` + `namedExports` into `exports`[/-] [+]mock.module: consolidate `defaultExport` + `namedExports` into `exports`[/+] on May 24, 2025
  2. cjihrig commented on May 24, 2025

    @cjihrig
    Contributor

    We are not aware of a reason to separate them

    CommonJS?

    EDIT: Actually, I guess CommonJS can be handled by this change.

  3. cjihrig commented on May 24, 2025

    @cjihrig
    Contributor

    After thinking about this more this seems like it might be a more confusing user experience (I might be misunderstanding how the proposed changes will work). If I provide the example mock exports:

    exports: {
      default: mock__default,
      foo: mock__foo,
    },

    Don't I now need to know more about the module that I'm mocking - specifically if it is CJS or ESM? If I'm mocking ESM, then I get a default export and a named export. However, if I'm mocking CJS, then I get two named exports, with one of them being named default. With the current API, I would get the same configuration for both module systems without needing to know the module type.

    it's inconsistent with other major testing frameworks/utilities (such as Jest).

    Another note here is that quibble, for example, does separate the named and default exports. Jest is also not known for its great support for both module systems.

  4. JakobJingleheimer commented on May 27, 2025

    @JakobJingleheimer
    MemberAuthor

    With the current API, I would get the same configuration for both module systems without needing to know the module type.

    Ahh, interesting. Buut I don't see how this could be possible:

    {
      defaultExport: mock__default,
      namedExports: { foo: mock__foo },
    }

    In CJS, wouldn't mock__default get assigned to module.exports (module.exports = mock__default), and then foo would get added onto mock__default (Object.assign(module.exports, { foo: mock__default })). Then you effectively end up with a function with an instance property as the export of the CJS module? (I think it could not happen in reverse because then one would squash the other)

    import someCjs from 'some-cjs';
    // 💥 import { foo } from 'some-cjs';
    
    typeof someCjs; // 'function'
    typeof someCjs.foo; // 'function'

    (I'm not sure what happens via require here: const { foo } = require('some-cjs'); I think it probably works as expected?)

    And if the mocked module was ESM:

    import someEsm, { foo } from 'some-esm';
    
    typeof someEsm; // 'function'
    typeof foo; // 'function'
    typeof someEsm.foo; // 'undefined'
  5. cjihrig commented on May 27, 2025

    @cjihrig
    Contributor

    Good point. Yes, that's how it works. I don't think you can really avoid it in CJS. We could do the same thing in ESM under the hood, but I don't think that would be a good idea. I still think the current behavior is significantly less confusing than my example in #58443 (comment).

    Currently, when the mock is created, foo is specified as a named export. If I wanted to access a named export, I would use the correct syntax to do that. If I planned to access it as someEsm.foo, I would have created the default export properly.

    This has also been the behavior since the API was introduced. We shouldn't break existing users just because an API is experimental unless there is a good reason to do so - we saw how bothered people got with the loader hooks changes.

    I also don't think creating a automated migration and changing the API in v25 would be adequate because there wouldn't be a consistent API across all of the supported versions. I think you would need to wait until there was a single usable API on all supported release lines before removing things.

    I guess I don't see any tangible upside to this proposed change, but I do see disadvantages.

  6. JakobJingleheimer commented on May 28, 2025

    @JakobJingleheimer
    MemberAuthor

    Actually, I was wrong: it can be done to work the same:

    Details
    // foo.cjs
    module.exports = function foo() {}
    module.exports.a = 'a';
    // does not work via `Object.assign(module.exports)`
    // main.mjs
    import foo, { a } from './foo.cjs';
    console.log({ foo, a });
    {
      foo: [Function: foo] { a: 'a' },
      a: 'a',
    }
    

    I believe this can be applied to both options shapes (defaultExport + namedExports and exports) if it isn't already applied to the current.

    This has also been the behavior since the API was introduced. We shouldn't break existing users just because an API is experimental unless there is a good reason to do so

    One impetus is facilitating adoption of node's test runner. @nodejs/userland-migrations is working on a migration from Jest. This would help with that. Jest is by far the biggest test runner and test utilities lib (Jest's 29M vs Quibble's 135K / Testdouble's 127K).

    Another is ergonomics (I think this is an ergonomic improvement—no shade to whoever chose the current). If we provide an automated migration, the burden to users seems trivia; for me as a user, I would happily run a quick script once to improve my DX going forward (I think we should include info about the migration in the deprecation message). I would also be okay leaving the separated ones for longer.

    I wish we had a way to track usage so we know can judge user-preference (but that's another can of worms). BUT we can know how many times the migration is used, which could give us an idea.

    we saw how bothered people got with the loader hooks changes.

    Touché 😅 This is significantly smaller/less complex and (IMHO) far less controversial, especially if we provide a migration. We can also do a better job this time advertising the incoming change.

  7. cjihrig commented on May 28, 2025

    @cjihrig
    Contributor

    If we can make both APIs work the same, I don't care too much about the actual format of the options object. I do care about breaking users though. I think an alternative plan could look like this:

    1. Immediately introduce options.exports
      • alias defaultExport & namedExports into options.exports
      • Copy the existing test file to use the API from this issue to make sure we can cover all of the same cases.
    2. Optionally, provide a userland-migration to automatically transform things. Since this is a userland thing, I'm not too concerned about this.
    3. Wait until there is a consistent API on all supported release lines to consider removing anything.

    As a side note, I think always trying to do whatever Jest does in general should be a non-goal.

  8. JakobJingleheimer commented on May 28, 2025

    @JakobJingleheimer
    MemberAuthor

    That sounds good to me!

    I think always trying to do whatever Jest does in general should be a non-goal.

    I don't either 😉 In this case, I think they got it right though.

  9. Taiseer22 commented on Jan 20, 2026

    @Taiseer22
  10. 12 remaining items

  11. github-actions commented on Sep 6, 2026

    @github-actions
    Contributor

    This issue has been marked as stale due to 90 days of inactivity.
    It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.

  12. added
    staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.
    on Sep 6, 2026
  13. filmaj commented on Sep 6, 2026

    @filmaj

    Please do not mark as stale, this is worth addressing 🙏

  14. JakobJingleheimer commented on Sep 6, 2026

    @JakobJingleheimer
    MemberAuthor

    This is actually already implemented and can be closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.test_runnerIssues and PRs related to the test runner subsystem.

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions