Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

moduleNameMapper - mocks do not work for imported modules of mapped modules using "moduleNameMapper" #4262

Closed
alex-mironov opened this issue Aug 13, 2017 · 30 comments · Fixed by #7787

Comments

@alex-mironov
Copy link

alex-mironov commented Aug 13, 2017

I use jest with webpack. Webpack is configured to use alias for some imports:

    alias: {
      shared: path.resolve(process.cwd(), "some-lib/src"),
    },
    modules: ["app", "node_modules", "some-lib"],

some-lib is git submodule added to the project.

part of Jest config from package.json:

    "moduleDirectories": [
      "node_modules",
      "app",
      "shared-lib"
    ],
    "modulePaths": ["<rootDir>/shared-lib"],
    "moduleNameMapper": {
      "^api(.*)$": "<rootDir>/shared-lib/src$1"
    },

When I'm trying to mock imported module in jest it doesn't work

    jest.mock("shared/utils")
    import { utilFunc } from "shared/utils"

as a result utilFunc is unmocked.
I'm using the latest version of jest.

@lgraziani2712
Copy link

I think "it works" but it's broken maybe?

Example 1: it works

Module with something to mock
// src/moduleWithSomethingToMock.js
import moduleToMock from '../../rootAlias/helpers/moduleToMock';
Test
import moduleWithSomethingToMock from './moduleWithSomethingToMock';

// Here I'm using the alias
jest.mock('rootAlias/helpers/moduleToMock');

Example 2: doesn't work

Module with something to mock
// src/moduleWithSomethingToMock.js

// Here I'm using the alias
import moduleToMock from 'rootAlias/helpers/moduleToMock';
Test
import moduleWithSomethingToMock from './moduleWithSomethingToMock';

// Using or not the alias, it doesn't mock the module
jest.mock('rootAlias/helpers/moduleToMock');

@thymikee
Copy link
Collaborator

Does it happen on jest@test?

@lgraziani2712
Copy link

It does 😿

@shikaan
Copy link

shikaan commented Feb 1, 2018

If someone can find it interesting: in my case the problem was that the order of entries in moduleNameMapper was wrong... Children should always be before parents in that list.

@vitalybe
Copy link

I had experienced similar issues with jest v20.0.4 upgrading to v22.2.2 solved that issue.

@timtrinidad
Copy link
Contributor

timtrinidad commented Feb 16, 2018

We're hitting this issue as well - upgrading to 22.2.2 or downgrading to 20.0.4 didn't work.

Some details:

  • This works properly if you pass a factory (second argument) to jest.mock()
  • I've tracked the issue down to jest-runtime.requireMock(). Specifically:
    • If you pass in a relative path that doesn't need to be mapped, manualMock gets set to null, which eventually calls this._generateMock(from, moduleName);
    • If you pass in a path that needs to be mapped, manualMock gets set to the absolute FS path (even though this isn't a manual mock). This causes _generateMock to not be called.
  • There also seems to be some wonkiness since in some cases manualMock is a string/path, and in other cases (inside if (fs.existsSync(potentialManualMock))) it's a boolean with the path in modulePath.

Unfortunately, that's as far as I got. I attempted a fix but since I've only started working with this framework every change I made broke a bunch of tests...

Hopefully this helps someone find the true fix. I'm also happy to help put in a PR with some direction on what the fix should be.

@butlermd
Copy link

butlermd commented Mar 1, 2018

First and foremost, thanks to @timtrinidad for the workaround (passing a factory to jest.mock()).

Second, expanding on what @timtrinidad found, I'm on 22.4.0 of jest-runtime and took a look. The code does roughly the following:

  1. manualMock is resolved to a path by this._resolver.getMockModule(from, moduleName);
  2. Checks if the mock is already registered with this._mockRegistry.
  3. Checks if there's a factory registered with this._mockFactories and calls (and returns) it if there is.
  4. _gracefulFs is used to check if there's a manual mock defined in a __mocks__ directory and if there is, manualMock is set to true (a boolean rather than a string) and modulePath is changed to the path to the manually mocked module.
  5. A conditional checks the truthiness of modulePath (it's never falsy unless there's no moduleName which... unlikely) and if it's truthy, executes the module at module at module path with this._execModule(). The newly minted module (the real thing, not a mock), is then registered with the mock registry.
  6. The else for the above conditional appears to actually mock the module and register it with the mock registry.
  7. The mock is retrieved from the mock registry and returned.

So, there's a couple problems here. First, at step 4, it's determined whether or not we're dealing with a manual mock. In the use case that we're discussing here, we're not. But manualMock is never set to something falsy. Easy fix; just add an else to the conditional. That calls this._generateMock() which is the method we want called.

However, we have a new problem. The first line of _generateMock(from, moduleName) is const modulePath = this._resolveModule(from, moduleName);. That method call eventually kicks things to the jest-resolver method resolveModule() which in turn calls this.getModule(). That method tries to find the module, but doesn't take into account the moduleNameMapper configuration. So, this.getModule() returns null and then resolveModule() blows up and the module can't be loaded. Tests explode. People run screaming. Chaos reigns.

@butlermd
Copy link

butlermd commented Mar 1, 2018

I took a walk and tried something when I got back. I changed this._generateMock(from, moduleName) to this._generateMock(from, modulePath); and it works.

Rolled the two lines I changed back and working on a test. Hopefully I'll have a PR shortly.

@butlermd
Copy link

butlermd commented Mar 5, 2018

Having trouble running the tests. Reached out on discord. Once I can sort that out, I can submit the PR.

@butlermd
Copy link

butlermd commented Mar 6, 2018

I can't get the tests to run on my machine and haven't had any luck getting help with them. I was able to run them with my changes on Travis and my change breaks other things. Without being able to run them locally, it'll take way more time than is worth it to get a fix that doesn't break the existing tests. So, I'm shelving this for the time being.

@jamietre
Copy link
Contributor

jamietre commented May 18, 2018

Does anyone have any new information about this, or know whether it's going to be fixed before the next release? The same problem occurs when using a custom resolver. We use aliases/nonrelative paths almost exclusively so this is a big issue for us adopting jest. As of 23.0.0-charlie.2 it's still not working for me.

jest.mock('some/module') is able to resolve the module (e.g. does not throw an error) but it doesn't get automocked; the original is still used. Using an inline mock works, e.g.

jest.mock('some/module', () => {
    return {
      foo: jest.fn()
    };
});

I've tried to reproduce it in isolation but haven't been able to yet, using both relative & aliased/rooted paths for the original import. So it's doesn't seem like it's quite as simple as how @alex-mironov initially observed it, or maybe it manifests differently for when using a custom resolver?

Additional info:

After a bit of tracing I think this is a result of circular dependencies, which while probably not good are valid in ES6 modules. If I can repro in isolation will file a specific issue.

@jamietre
Copy link
Contributor

jamietre commented May 18, 2018

Update:

Figured out the cause here. I'm not actually sure if this is a bug. I have a module that depends on another module which fails when loaded in the testing environment, because of globals that aren't defined. Automocking tries to load all the modules, and fails when it gets to a dependent module that fails while loading.

So for example

module1 -> depends on module2
module2 throws an error when loaded

The test code has

jest.mock('module2')
jest.mock('module1')

I would have expected that module2 wouldn't be loaded since I already mocked it. But during the resolution process of module1, even though I've automocked module2, it still loads it during this process, and it fails. If I provide a manual mock, it wouldn't try to load module2 so it works:

jest.mock('module2', () => { return {} });
jest.mock('module1')

This makes sense, in a way, but I guess a future enhancement might be to use an AST to parse the public API of mocked modules instead of trying to load them. Having to actually load a module to mock it seems likely to fail in some scenarios. It's possible I just don't understand how this feature is expected to be used.

Another enhancement would be to provide output that the test failed during the module resolution/mocking process-- it's not at all clear that's what's happening; when looking at the stack trace, it just looks like the module didn't get mocked, but it's actually failing while trying to load dependencies of the module during mocking.

In any event the problem I'm having doesn't seem to be related to this bug any more, so it's possible this has been fixed already.

@cruhl
Copy link

cruhl commented Jul 19, 2018

Hello everyone, I just ran into this issue today. Is there anything I can do to help push this issue forward?

@Hotell
Copy link

Hotell commented Aug 23, 2018

got the same issue

image

[email protected]

@SimenB
Copy link
Member

SimenB commented Aug 23, 2018

@jamietre

This makes sense, in a way, but I guess a future enhancement might be to use an AST to parse the public API of mocked modules instead of trying to load them. Having to actually load a module to mock it seems likely to fail in some scenarios.

That's doesn't really seem feasible - to actually support all ways of exports.bla =, module.exports =, module.exports.bla = not to start with if (maybe) module.exports = and whatever export might be transpiled to from different dialects, seems to me like an endless rabbit hole. Maybe if we enforced export syntax, but that isn't really widespread yet, especially for node_modules. And having that constraint before proper ESM support in Jest also seems weird.

Another enhancement would be to provide output that the test failed during the module resolution/mocking process-- it's not at all clear that's what's happening; when looking at the stack trace, it just looks like the module didn't get mocked, but it's actually failing while trying to load dependencies of the module during mocking.

That makes a lot of sense to me, would you mind creating a PR for that? Maybe provide some hint that you might not be able to rely on automocking and that you have to provide a mock factory inline?

@jamietre
Copy link
Contributor

jamietre commented Aug 23, 2018

That's doesn't really seem feasible - to actually support all ways of exports.bla =, module.exports =, module.exports.bla = not to start with if (maybe) module.exports = and whatever export might be transpiled to from different dialects, seems to me like an endless rabbit hole. Maybe if we enforced export syntax, but that isn't really widespread yet, especially for node_modules. And having that constraint before proper ESM support in Jest also seems weird.

You're completely correct of course - I write almost exclusively ES201?/TypeScript these days and when I wrote that, in my mind, a module's public API would always be well-defined ;)

That makes a lot of sense to me, would you mind creating a PR for that? Maybe provide some hint that you might not be able to rely on automocking and that you have to provide a mock factory inline?

I can take a shot for sure when I get a little free time.

@Kruptein
Copy link

I've made a minimal example repo where the ModuleNameMapper does not resolve correctly for imports. https://github.com/Kruptein/jest-test-stuff

Running 'npm run test' results in:
Track.test has behaviour I expect but uses relative imports, whereas the TrackExpected.test is the behaviour with mapped names with wrong behaviour.

Additionally this output shows that something does not seem to resolve correctly:

Cannot find module '@@storage/track/Track' from 'TrackExpected.test.ts'

      4 | it('', () => {
      5 |     const track = new Track();
    > 6 |     console.log( jest.genMockFromModule('@@storage/track/Track'))
        |                       ^
      7 |     console.log( jest.genMockFromModule('../../../src/storage/track/Track'))
      8 |     // console.log(typeof track);
      9 |     // console.log(typeof (Track as any).mock);```

@steven-pribilinskiy
Copy link

steven-pribilinskiy commented Jan 15, 2019

I have to confirm that currently Jest has serious issues with aliases defined in a webpack config:

  • jest.mock('aliased/module') - doesn't do automocking
  • jest.genMockFromModule('aliased/module') - cannot find the module

This issue is 1.5 years old. Will it be ever fixed?

@miluoshi
Copy link

I can confirm I have the same problems stated by @pribilinskiy.

@SimenB
Copy link
Member

SimenB commented Jan 21, 2019

aliases defined in a webpack config

Jest does not support webpack aliases - you need to use moduleNameMapper (which is what this issue is about).

That said, I dug into this a tiny bit, and I'm not sure why it fail 😅 However, I've put together a failing integration test based on @Kruptein's excellent reproduction (removing typescript and logging), so maybe it will lower the barrier for people wanting to send a PR to fix this 🙂 Please see #7668

@steven-pribilinskiy
Copy link

We import a shared json file with aliases in webpack.config.js and jest.config.js

@grosto
Copy link
Contributor

grosto commented Jan 30, 2019

I can try to tackle this one.

@steven-pribilinskiy
Copy link

@grosto that would be awesome 👍

@dianjuar
Copy link

dianjuar commented Apr 27, 2019

I was facing this issue with

{
  "jest": "23.5.0",
  "ts-jest": "23.1.3"
}

I just update both dependencies to 24.7.1 and 24.0.2 and solved

@mediafreakch
Copy link

mediafreakch commented Feb 28, 2020

I'm having this issue with [email protected], [email protected] and @vue/[email protected].
Adding moduleNameMapper: { '@/(.*)$': '<rootDir>/src/$1' } to jest config doesn't make a difference...

UPDATE:
It seems I was looking at an error message due to a different reason.
Mocking modules with jest.mock('@/myPath') actually works out of the box with vue-cli-plugin-unit-jest. You don't even need the moduleNameMapper setting.

@ariesjia
Copy link

is there any solution for this issue

@JulienKode
Copy link

JulienKode commented May 16, 2020

I have the same issue, want to use moduleNameMapper to alias an external module, seems not to work

  moduleNameMapper: {
    '@stuff(.*)$': '@scope/stuff',
  },

@thom801
Copy link

thom801 commented Dec 31, 2020

Possible solution for some of you:

If it's any help to anyone, I just got mine working with this:

In my file that is importing the module:

import { TransactionEmail } from 'mail/transactional-email'

Then in my jest config:

 moduleNameMapper: {
    // Force mock import.
    "mail/transactional-email": "<rootDir>/src/mail/__mocks__/transactional-email.ts",
    // Normal module aliases.
    "^mail/(.*)": "<rootDir>/src/mail/$1",
    "^api/(.*)": "<rootDir>/src/$1"
  },

And then you don't call jest.mock() anywhere, you are basically just creating an alias for that import in the config and that's it.

For me, I am on latest versions of jest/ts-jest but when I call jest.mock('mail/transactional-email') it does not mock the module, it just fails silently and continues with tests, but the real module gets imported.

To force jest to import the mock module I just mapped the module import path directly to the mocked module. Not exactly a great solution, but I wanted to share regardless if anyone is in a pinch.

To be honest, the docs for module mocking are really confusing in my opinion I think a big part of the issue here might just be outdated or difficult to understand documentation. My understanding here was that if you say to mock src/modulename then ANY import in the jest test environment should look for a mock file in a sibling directory of the imported module named __mocks__ and look for modulename in there instead. Not sure if that is actually the case or not but would love someone to clarify.

I hope this helps someone else! 🍻

@POD666
Copy link

POD666 commented Apr 18, 2021

Could you reopen the issue?
Looks like the PR that should fix it actually didn't: #7787 (comment)

I use the same workaround as @thom801 for now:

../src/mocks/store.js:

module.exports = {};

.../test/jest/jest.config.js:

...
moduleNameMapper: {
        // __mocks__
        '^src/store/index(.*)$': '<rootDir>/src/__mocks__/store.js',
        // src
       ...
...

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 19, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.