jest: jest.mock does not mock an ES module without Babel
š Bug Report
In an ES module Node project, with no Babel, jest.mock
works when the mocked module is a node_modules
package that exports CommonJS, but it isnāt working for me mocking an ES module exported from a file in the same project.
(Itās possible that an NPM package that only exports ES modules has the same issue. I didnāt try that case.)
To Reproduce
Steps to reproduce the behavior:
Click Run in the repl, or hereās a simple example:
// main.js
import secondary from "./secondary.js";
export default function main() {
return secondary();
}
// secondary.js
export default function secondary() {
return true;
}
// test.js
import { jest } from "@jest/globals";
jest.mock("./secondary.js");
let main;
let secondary;
beforeAll(async () => {
({ default: main } = await import("./main.js"));
({ default: secondary } = await import("./secondary.js"));
});
test("works", () => {
secondary.mockReturnValueOnce(false); // TypeError: Cannot read property 'mockReturnValueOnce' of undefined
expect(main()).toBe(false);
});
Expected behavior
jest.mock(filename)
should mock the exports from filename
when the test file and the Node project are both ES modules (type: "module"
)
Link to repl or repo (highly encouraged)
https://repl.it/repls/VerifiableOfficialZettabyte
envinfo
System:
OS: macOS 10.15.4
CPU: (4) x64 Intel(R) Core(TM) i5-4278U CPU @ 2.60GHz
Binaries:
Node: 12.16.3 - ~/.nvm/versions/node/v12.16.3/bin/node
Yarn: 1.21.1 - /usr/local/bin/yarn
npm: 6.14.4 - ~/DevProjects/reaction/api-utils/node_modules/.bin/npm
npmPackages:
jest: ^26.0.1 => 26.0.1
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 174
- Comments: 115 (22 by maintainers)
Commits related to this issue
- Set up spys instead of using mock jest.mock doesn't work work against ES6 modules, but this approach of using spys instead does. See: https://github.com/facebook/jest/issues/10025 — committed to candrews/mdx-mermaid by candrews 3 years ago
- Upgrade unist-util-visit to 4.1.0 (#19) * Set up spys instead of using mock jest.mock doesn't work work against ES6 modules, but this approach of using spys instead does. See: https://github.c... — committed to sjwall/mdx-mermaid by candrews 2 years ago
- chore: begin to implement esm conversion Blocked by legit support in jest for mocking esm modules: https://github.com/facebook/jest/issues/10025 — committed to stdavis/good-samaritan by stdavis 2 years ago
- build: change packaging to esm (#62 #19 #18) * Upgrade unist-util-visit to 4.1.0 (#19) * Set up spys instead of using mock jest.mock doesn't work work against ES6 modules, but this approach of ... — committed to sjwall/mdx-mermaid by sjwall 2 years ago
- ESM Test Mocking Trying to resolve the rest of the issues with the ESM rewrite of the Jest tests. Making the mocking procedures ESM-compliant was part of the holdup, at least in terms of that it uses... — committed to Offroaders123/jsmediatags by Offroaders123 a year ago
I respectfully disagree with this being labeled a feature request. Itās entirely blocking of any effort to move to Jest native ES module support if any files have mocks in them, and there is no workaround that I know of (other than to continue using CommonJS through Babel, which means that ES module support is broken, hence bug).
One thing to note is that it will be impossible to mock
import
statements as they are evaluated before any code is executed - which means itās not possible to setup any mocks before we load the dependency. So youāll need to do something like this usingimport
expressions.It will be a bit cleaner with top-level
await
Any modules loaded by
someModule
viaimport
statements would work though as weād have time to setup our mocks before that code is evaluated.The example in the OP follows this pattern, Iām just pointing it out š
I started working on this, and I think it makes sense to leave
.mock
and.doMock
for CJS, and introduce a new.mockModule
or something for ESM. It will require users to be explicit, and allow the factory to be async. Both of which I think are good things.Also need to figure out
isolateModules
. Unfortunately it uses themodule
name while not being for ES modules.@thymikee @jeysal thoughts?
As a status update, Iāve opened up a PR here: #10976
Is a fix for this still on the roadmap?
@yurijmikhalevich do you have any update, please?
For anyone wanting to use this functionality I created a demo repo here (including TS support via
ts-jest
)https://github.com/connorjburton/js-ts-jest-esm-mock
Alright Iāve figured out how to mock the default as well
Turns out @Pauan was right all along, just needed to do some extra fixes
Thanks everyone for the help! ā¤ļø
@yurijmikhalevich is that going to be merged anytime soon?
Iām trying this out im didāt find anyway of doing partial mocking, using
requireActual
givesMust use import to load ES Module
And trying to do a partial import before ābreaksā the mock is the module is already imported
Has this been fixed in v28.0.0?
Hey all, here is what I decided to do, in case anyone finds it usefulā¦ Iām allowing manual dependency injection into my module functions via arguments, to allow for mocks, but setting default argument value to the normally imported dependency. I personally feel like this is reasonable given the circumstances. Example:
@GerkinDev Hello, Iām experimenting with using pure ESM for testing, and find myself in the same situation as @jasonrberk. However, your suggestion isnāt working for me to mock a module (
fs
in this case) thatās being loaded by the module being tested.Hereās a minimal example:
This test fails - the real
fs
gets used, not the mock. Do you have any idea why that might be the case? Thanks!Edit: I dug around a bit more and see that this PR is supposed to have replaced
jest.unstable_mockModule
withjest.mockModule
in 27.1.1. However, if I try to use that in my test with 27.4.7, I get ājest.mockModule is not a functionā, which is confusing. It feels like that probably relates to this problem, though.@anshulsahni no workaround, apart from rewriting your code to not use mocking for the moment with the esm modules
@SimenB this is my first comment in this repo, so the first words unambiguously look like - great work!
Weāre in one step from transitioning of our infrastructure into ESM. The only things left are tests. Weāre planning to actively use top level await in our code base and there is an obstacle because weāre ready to compile our code to ESNext (in terms of TS) but most of our tests use
jest.mock()
somehow and I want to understand the current state of affairs.After closing #9860 by #10823 there is one important topic left considering original list in #9430 - support of
jest.(do|un)mock
(OK, to be honest, probably there is another one - Package Exports support).Can You explain the current status of the issue?! I mean:
jest.mock
(jest.mockModule
?) with ESM now? (probably using27.0.0-next.x
?) And if so - can you provide a short working example?jest.mockModule
or ā¦?)Thanks in advance.
Thanks @connorjburton ! The key is to await import your SUT (after
jest.unstable_mockModule
) and not just import it.const index = await import("./index");
Then you are mocking things that are imported inside your SUT (index in case of example above)I think the documentation is lacking the this information and it is not really obvious https://jestjs.io/docs/ecmascript-modules
so is it safe to say, that the ONLY way you can actually do this: https://jestjs.io/docs/mock-functions#mocking-modules is by using babel to convert everything back to CommonJS? Iām so against having to involve babel in my relatively small project, i think it would just be easier to convert the entire project back to CommonJS and just wait for the testing frameworks to catch up.
please correct me if Iām wrong in my understandingā¦
also, as an aside, it would be really helpful to have a disclaimer on https://jestjs.io/docs/mock-functions#mocking-modules that youāll still need babel for your pure Node project before you build the whole thing and then see this:
https://jestjs.io/docs/ecmascript-modules
@kevinhooke but isnāt that conclusion logical in general i mean the basics of the engine are the event loops so it is total clear that import() is a async operation and it will not be ready in the same cpu cycle that is a total expect able result.
If you as a ECMAScript dev get stuck on issues like that you need to learn the fundamentals about the JS Loop and the stack handling else you will shoot your self in the foot later anyway.
out of the above pictures the conclusion is clear as soon as i await something it is there after that. not before that. so import will only enqueue it while await will wait for the result hope that makes sense.
There IS (sometimes) a workaround, but it requires you to go about things in a very specific way:
outdated: there's a better way (below)
In the above,
cp.default.fork
is a mock that returns an empty object.Note that
cp.fork
cannot be mocked this way because it is a direct export (which ESM protects), and even whencp.default.fork
is mocked,cp.fork
still is NOT becausecp.default.fork
has been re-assigned to the mock; its original value (the named exportfork
) is unaffected. Under the hood, the child_process module is doing something likeNote that child_process is CJS under the hood, but that doesnāt matter:
export default {ā¦}
works from ESM because the default export is an object whose properties are not protected.This works because ESM does not protect properties of exported objects, only the export itself.
For the mock to be present in the top-level scope (eg when imported), you must use
await import
in the test file to enforce sequence (including the qux import!).@SimenB before your PR gets merged, what is the work-around solution here?
@8o8inCodes When you do
await import("foo")
it gives you the ES6 module, not the default export. The ES6 module is an object that contains all the things which are exported from the module.So if the module is exporting something called
bar
then you need to use(await import("foo")).bar
to access it.And if the module does
export default
then you have to access it with(await import("foo")).default
.The default export isnāt special, itās just an ordinary export called
default
. When you do thisā¦That is exactly the same as doing this:
So you see that the default really is just an ordinary export called
default
.I think it should be
const MyClass = (await import('./MyClass')).default;
not related to Jest. TestDouble.js is an entirely different ES6 testing framework:
https://github.com/testdouble/testdouble.js/
https://github.com/testdouble/testdouble.js/tree/main/examples
Thanks,
Jason Berk @.***
On Sun, Aug 14, 2022 at 3:53 PM Wing Chau @.***> wrote:
My previous post is not good enough to understand, so I rewrite and post about it again.
As you know, the Jest example of manual-mocks work correctly with Babel. I have tried modifying to work that example without Babel, and I found the follows:
jest.mock()
works: when ES Module require CommonJS module with Node.jsmodule.createRequire(filename)
methodrequire
Manual mock CommonJS module of fs.cjs.jest.mock()
dose not work: when MS Moduleimport
ES Module.import
Manual mock ES Module ofmodels/user.mjs
,user.getAuthenticated()
return the value ofmodel/user.mjs
instead of that ofmodel/__mocks__/user.mjs
.After the above the 2nd problem would be fixed, I could use
jest.mock
with Manual Mocks ofimport('node:fs/promise')
.For further information, see my repository.
I am trying to find workaround for Jest ESM support without Babel.
Based on the Jest examples/manual-mocks, I modified the source and configuration files, and found the follows:
I can not find how to use jest.mock for ES Modules import.
My example project is juggernautjp/jest-manual-mocks-example, so you can modify and verify by downloading from my repository.
If someone has any idea to fix my bugs, will you please let me know?
I was unable to get TypeScript + ESM + mocks to work and ended up transpiling to CJS with Babel for tests.
For anyone still trying to have testable code with an ESM codebase (i.e.,
{ "type": "module" }
inpackage.json
), maybe my config will work for you too.Install the dependencies:
jest.config.json:
The
setupFiles
are specific to my setup. You can probably remove or update that for your own.moduleNameMapper
strips the.js
and.jsx
extensions from module names, which is necessary because ESMimport
statements need the file extension, while therequire
statements produced by Babel do not.babel.config.json:
If youāre using ESM, youāll probably use
import.meta.url
at some point, which is unavailable in CJS code. The two Babel plugins above transformimport.meta.url
statements into something CJS can use.For example, maybe you need
__dirname
, which is unavailable in ESM:Or maybe you want to check if the module was the entry point for the Node process:
https://github.com/facebook/jest/issues/9430#issuecomment-915109139
you can show me your config please? my apps runs in esm modules and I need run my test with commonjs modules
Your callback inside of
jest.unstable_mockModule
does not return anything. If the intention is to return an object, you should wrap it in parenthesis:You donāt need
babel
, see my example repo here, you only needjest
Thatās exactly what happens when you use static
import
, the dependency is guaranteed to be loaded before your code runs. For exampleā¦But if you use
import("some-module")
then you are doing dynamic runtime loading, so you need to useawait
or.then
in order to wait for the module to finish loading.Most JS code should use static
import
, dynamicimport()
is a niche feature that is useful in some situations, but it is not the norm. Unit tests are one of those niche situations whereimport()
is useful.Not sure when it landed but
jest.unstable_mockModule
and dynamic import works as expected@PeterKDex exactly that is what you should do it is always a good pattern to allow your module dependencies to be inject able.
Hi all, great work on the ESM implementation so far. Only took a couple of hours to get up and running with a project that uses node natively, with no code transforms
Iāve found an issue with
jest.unstable_mockModule
where the factory function is called once per import of mocked module found in the code.So if I have two files, both importing the mocked module, theyāll each get a separate instance. And then this can cause issues if importing the mocked module into the test case, itās not guaranteed which of the instances you will get. In my case, the instance imported into my test file is not the one that contains the right data in
.mock.calls
So far the workaround is to reorder a few
imports
in my source code, such that the module making the call to the mocked module is imported last.The
ora
-package exports a function that when called with its parameters returns an object that has asucceed
and astart
function (among others). What you currently return is what the aforementioned function returns instead of the function. It should be something like this:But I donāt think issues on how to mock specific libraries is something that should be discussed in this issue tracker.
Are there any updates on mocking ESM without the factory function? For example, vitest supports creating mocks for ESM automatically.
Is there a roadmap which shows when jest will fully support ESM?
Then this may be a ādefaultā issue. This is the reason I always prefer named exports š
@jasonrberk Fact is, if you use jest, you use babel already: https://jestjs.io/docs/next/code-transformation#defaults.
Depending on your complete stack, it can be not that hard to mock modules. With typescript,
ts-jest
includes presets that requires just a bit of config for allow it to handle node_modules if required. Then, to work with the hoisting mechanism ofbabel-jest
that is broken with ESM (since static imports are resolved before running the script for what Iāve seen), you just have to use top-level await to import files importing the modules to mock.Example plagiating @SimenB above:
You have the following structure:
In
src/foo.ts
:In
src/foo.spec.ts
:Long story short: you use jest, you use babel if you donāt explicitly disable transforms, AFAIK. It does most of the heavy lifting out of the box already.
If anyone is interested in giving
jest.mockModule
from this PR ā https://github.com/facebook/jest/pull/10976 ā a try right now, here is a small example using it: https://github.com/yurijmikhalevich/esm-project-with-working-jest-mock.@akdev5 - Letās take this conversation somewhere else since itās not related to this issue. Maybe open a Stack Overflow issue and include as much detail and code snippets as you can and others might be able to help too!
A few additional tweaks to
config/__tests__/config.test.js
did the trick!@skilbjo - You are a life saver. Donāt event want to say how many hours Iāve been wrangling with this.
@IgorBezanovic I just experienced the same issue you had; hereās how I fixed it.
src.ts
test.ts
the trick was in
test.ts
before:
after:
So, what I see here is that your default entry is not an object, itās a key pointing directly to the function, can you try doing it like this:
default: { nameOfFunc: jest.mockReturnValue(mockValue) }
Thatās how I mocked all my modules, because default export is actually an objectAlso, for it to work, you need to than import this function with await statement after the mock was done, not using the normal import statement at the top of the file so for instance: const logger = await import(āsrc/utils/loggerā); And put this line after the mock is registered
@swissspidy I was following this answer by āstoneā
Regardless using ānewā on a function should work
However the mocking doesnāt return the function, it returns:
Module {Symbol(Symbol.toStringTag): 'Module'}
Thatās the reason it fails, but not sure why itās returning the symbolYouāre returning a function in your mock instead of a class, so that error makes sense
considering to use vitest, mock esm the same way mocking cjs in jest
Replace
import MyClass from ā./MyClassā;
With
const MyClass = await import(ā./MyClassā);
After! Unstable mockmodule
Lately I have migrated a medium project to ESM. And use āunstable_mockModuleā.
The order in the spec file is:
For me it very intuitive. No hoisting no black magic. Just works. šš
@frank-dspeed I agree with your explanation but I disagree with your statement that is it ālogical in generalā. Coming from other languages, if I import a dependent module I would expect it to already be loaded by the time I reference it in the following code, and not that it may or may not be loaded and introduce random behavior into my code. Iām nitpicking at how ECMAScript imports work within an event loop based architecture, but ālogical in generalā, no, it is not.
After trying the approach from the docs, and every combination suggested in this post and other posts online for several hours, I can conclude that this is the key to getting this to work.
Hereās the import section of my test file. Jest reports
SyntaxError: The requested module 'electron' does not provide an export named 'BrowserWindow'
if I havets-jest
configureduseESM: true
and run jest withNODE_OPTIONS=--experimental-vm-modules
.Tried different syntax and workaround but no luck at all. Itās clear to me that something in Jest is broken with ESM support enabled. Frustrated as Iāve converted all other code to native ESM syntax but canāt get Jest to work properly with it. Would be very happy to know if thereās any update on this issue.
@neophyt3 if you want to test something fresh check out the new nodejs vm api it allows shiming ESModules
most of the vm.<methods> got now a importModuleDynamically parameter that allows you to set a callback for import()
hope that helps
Background
the vm module in nodejs got build to implement custom userland module systems š
https://nodejs.org/api/vm.html#vmrunincontextcode-contextifiedobject-options
is what your looking for may god be with you
@vialoh I went with your approach to get around the lack of mocking. It looks like it worked out just fine. I will report back if I find any issues. I think this is the simplest approach until there is some kind of official solution from the Jest team.
Yeah! The true C# way. But automocking was soooooo convenient š
out of my view the mock implementation of jest simply needs a diffrent api for esm as it needs to address shared linked modules also!
i think the mock implementation that now trys to find a object via a string mock(āmoduleToMockā) is confusing anyway
simply taking a object and returning a proxy for it is enough to implement mocking.
As I mentioned above, it depends on your setup: some other preprocessors, like
ts-jest
, makes the thing much easier somehow (I just tried to dig in and didnāt found a reason. Maybe somebody else here knows how it does it almost like magic).In your case, in pure ESM, you can replace the beginning of your test file like the following:
Sorry for the confusion, I was confused by ts-jest doing stuff I didnāt expect it to do, that made things work in my tests here.
@thernstig probably yes. See my comment above for a potential workaround.