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

jest.mock does not mock an ES module without Babel #10025

Open
aldeed opened this issue May 11, 2020 · 115 comments
Open

jest.mock does not mock an ES module without Babel #10025

aldeed opened this issue May 11, 2020 · 115 comments

Comments

@aldeed
Copy link

aldeed commented May 11, 2020

🐛 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 
@aldeed
Copy link
Author

aldeed commented May 11, 2020

Copied from #9430 (comment) at the request of @SimenB. Thanks!

@aldeed
Copy link
Author

aldeed commented May 22, 2020

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).

@SimenB
Copy link
Member

SimenB commented May 30, 2020

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 the module name while not being for ES modules.

@thymikee @jeysal thoughts?

@guilhermetelles
Copy link

guilhermetelles commented Jul 15, 2020

@aldeed @SimenB Hello, I'm also having the same problem, but when I try to use jest with babel instead, I'm running into SyntaxError: Cannot use import statement outside a module (it throws inside an imported module), which is basically what #9430 is all about I guess.

Is there any workaround to mock modules from the same project? Or to prevent the SyntaxError from occurring when using babel .

@aldeed
Copy link
Author

aldeed commented Jul 16, 2020

@guilhermetelles It can be a pain to do, but you'll likely get more help if you create a public minimal reproduction repo for your issue and create a new GH issue that references this one. There are about a dozen things that could cause this issue, from using an older Node version to Babel config issues, and being able to see all the project files is the best way for someone to help you solve it.

@SimenB You mentioned above that you had a start on this and it looks like everyone 👍 your proposal. Is there any update?

@SimenB
Copy link
Member

SimenB commented Oct 26, 2020

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 using import expressions.

import { jest } from '@jest/globals';

jest.mockModule('someModule', async () => ({ foo: 'bar' }));

let someModule;

beforeAll(async () => {
  someModule = await import('someModule');
});

test('some test', () => {
  expect(someModule.foo).toBe('bar');
});

It will be a bit cleaner with top-level await

import { jest } from '@jest/globals';

jest.mockModule('someModule', async () => ({ foo: 'bar' }));

const someModule = await import('someModule');

test('some test', () => {
  expect(someModule.foo).toBe('bar');
});

Any modules loaded by someModule via import 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 👍

@stellarator
Copy link

@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:

  • Do we have a mechanism to use jest.mock (jest.mockModule?) with ESM now? (probably using 27.0.0-next.x?) And if so - can you provide a short working example?
  • If not - do you plan to release this or similar functionality in 27.0? And do you have a proposal? (jest.mockModule or ...?)

Thanks in advance.

@SimenB
Copy link
Member

SimenB commented Dec 8, 2020

I want to add jest.mockModule, but since that's a new API and not a breaking change it might not go into Jest 27 at release. A PR would be very much welcome, but it's getting into some of the nitty gritty of jest-runtime so I understand if people are a bit hesitant to attempt it 🙂

@SimenB
Copy link
Member

SimenB commented Dec 23, 2020

As a status update, I've opened up a PR here: #10976

@anshulsahni
Copy link

@SimenB before your PR gets merged, what is the work-around solution here?

@kalinchernev
Copy link

@anshulsahni no workaround, apart from rewriting your code to not use mocking for the moment with the esm modules

@SimenB
Copy link
Member

SimenB commented Feb 11, 2021

yeah, there is not workaround if you wanna use native ESM until that lands

@anshulsahni
Copy link

right now I'm using babel & it's configured to only convert esm modules. So the whole app runs in esm modules but tests run with commonjs module system

@SimenB
Copy link
Member

SimenB commented Feb 11, 2021

yep, that'll keep working

@mahnunchik
Copy link

I'm looking forward for this feature 👍

@victorgmp
Copy link

right now I'm using babel & it's configured to only convert esm modules. So the whole app runs in esm modules but tests run with commonjs module system

you can show me your config please? my apps runs in esm modules and I need run my test with commonjs modules

@mrazauskas
Copy link
Contributor

mrazauskas commented Mar 16, 2023

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:

jest.unstable_mockModule("../services/user.service.js", async () => ({
  createUser: jest.fn(),
}));

Offroaders123 added a commit to Offroaders123/jsmediatags that referenced this issue Apr 17, 2023
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 different Jest functions that are specific to ESM, which were different when previously compiling back to CJS as the endpoint.

Just to make things a bit easier to debug and understand, I moved the Node FS and XMLHttpRequest mocks into the tests themselves, since it doesn't appear that you can have a custom mocking folder structure, unlike which you can with regular tests.

The mocking still isn't fully working, but these resources helped lead me to this point now.

https://stackoverflow.com/questions/40465047/how-can-i-mock-an-es6-module-import-using-jest
https://remarkablemark.org/blog/2018/06/28/jest-mock-default-named-export/
jestjs/jest#10025
https://github.com/connorjburton/js-ts-jest-esm-mock
@stixx200
Copy link

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?

@SimenB
Copy link
Member

SimenB commented Apr 26, 2023

Are there any updates on mocking ESM without the factory function?
For example, vitest supports creating mocks for ESM automatically.

PRs welcome!

Is there a roadmap which shows when jest will fully support ESM?

The pinned #9430

@IdeaHunter
Copy link

I cant see any ticket for unmocking, is there any PR i should look for, or should i start my own?

@IgorBezanovic
Copy link

Hello,

My colleague and I have a problem with mocking default helper function.

We tried follow example:
import { jest } from '@jest/globals';

jest.unstable_mockModule('/helpers/exampleFunction.js', () => ({
__esModule: true,
default: jest.fn().mockReturnValue(mockValue)
}))

Behavior is not mocked, the function still call original helper function, does not consider the mock data.

Do you have some suggestions?
Community, tnx in advance!

@mrazauskas
Copy link
Contributor

Do you have some suggestions?

Create a minimal reproduction repo and open an issue. It is hard to say something useful looking at the snipped you provided.

@Nexi77
Copy link

Nexi77 commented Aug 17, 2023

Hello,

My colleague and I have a problem with mocking default helper function.

We tried follow example: import { jest } from '@jest/globals';

jest.unstable_mockModule('/helpers/exampleFunction.js', () => ({ __esModule: true, default: jest.fn().mockReturnValue(mockValue) }))

Behavior is not mocked, the function still call original helper function, does not consider the mock data.

Do you have some suggestions? Community, tnx in advance!

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 object

Also, 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

@IgorBezanovic
Copy link

@Nexi77
Yes, I exported function from helper like export default functionName;

Code:

jest.unstable_mockModule('/helpers/fileName.js', () => ({
  __esModule: true,
  default: { functionName: jest.fn().mockReturnValue(mockValue) }
}))
const functionName = await import('/helpers/fileName.js');

And still call original helper function which call 3rd party, instead to use mockValue

@skilbjo
Copy link

skilbjo commented Jan 6, 2024

@IgorBezanovic I just experienced the same issue you had; here's how I fixed it.

src.ts

import axios from 'axios'

const handler = async () => { 
  const result = await axios.request({ url: 'https://example.com })
  console.log({ result })
}

test.ts

jest.unstable_mockModule('axios', () => ({
  default: { request: jest.fn() },
}));

const src = await import('src');

describe('...', () => {
  it('axios is mocked', () => {
    const actual = await src.handler()

     expect(actual).toBeUndefined() // correct; if unmocked it would have been an AxiosResponse
  })
})

the trick was in test.ts

before:

import * as src from 'src'

jest.unstable_mockModule('axios', () => ({
  default: { request: jest.fn() },
}));

...

after:

// import * as src from 'src' // removed

jest.unstable_mockModule('axios', () => ({
  default: { request: jest.fn() },
}));

const src = await import('src'); // no longer `import * as src from 'src'`; and placed after `unstable_mockModule`

@mike-weiner
Copy link

mike-weiner commented Jan 6, 2024

@skilbjo

I've been banging my head against a wall, and I'm hoping that maybe you can help!

I am trying to mock the ora library.

These are the libraries installed:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "jest": "^29.7.0"
  },
  "dependencies": {
    "ora": "^8.0.1"
  }
}

I've set up a barebones project to try and get this to work. The directory structure is:

package.json
package-lock.json
config/
├─ config.js
├─ __tests__/
│  ├─ config.test.js

config/config.js

import ora from 'ora';

export default (options) => {
  const spinner = !options.raw ? ora('Reading Environment Variables').start() : undefined;
  spinner.succeed(`Environment Variables Retrieved`);
  return true;
};

config/__tests__/config.test.js

jest.unstable_mockModule('ora', () => ({
  default: { start: jest.fn(), succeed: jest.fn() },
}));

const src = import('../config');

describe('...', () => {
  it('ora is mocked', () => {
    const actual = config({})

    expect(actual).toBeUndefined() // Yes, I realize this is a terrible and incorrect unit test.
  })
})

When I run node --experimental-vm-modules node_modules/jest/bin/jest.js, I get the following:

/demo/config/config.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import ora from 'ora';
                                                                                  ^^^^^^

SyntaxError: Cannot use import statement outside a module
    at new Script (node:vm:99:7)
    at Runtime.createScriptFromCode (/demo/node_modules/jest-runtime/build/index.js:1505:14)
    at Runtime._execModule (/demo/node_modules/jest-runtime/build/index.js:1399:25)
    at Runtime._loadModule (/demo/node_modules/jest-runtime/build/index.js:1022:12)
    at Runtime.requireModule (/demo/node_modules/jest-runtime/build/index.js:882:12)
    at Runtime.requireModuleOrMock (/demo/node_modules/jest-runtime/build/index.js:1048:21)
    at Runtime.loadCjsAsEsm (/demo/node_modules/jest-runtime/build/index.js:726:22)
    at Runtime.resolveModule (/demo/node_modules/jest-runtime/build/index.js:684:17)
    at importModuleDynamically (/demo/node_modules/jest-runtime/build/index.js:1520:26)
    at importModuleDynamicallyWrapper (node:internal/vm/module:430:15)

Node.js v21.5.0

@skilbjo
Copy link

skilbjo commented Jan 6, 2024

@mike-weiner what happens if you add type: module in your package.json ?

@mike-weiner
Copy link

@mike-weiner what happens if you add type: module in your package.json ?

Interesting. I do get a slightly different error:

ReferenceError: jest is not defined

    > 1 | jest.unstable_mockModule('ora', () => ({
        | ^
      2 |   default: { start: jest.fn(), succeed: jest.fn() },
      3 | }));
      4 |

      at jest (config/__tests__/config.test.js:1:1)

@mike-weiner
Copy link

@mike-weiner what happens if you add type: module in your package.json ?

A few additional tweaks to config/__tests__/config.test.js did the trick!

import {jest} from '@jest/globals'

jest.unstable_mockModule('ora', () => ({
  default: { start: jest.fn(), succeed: jest.fn() },
}));

const config = await import('../config').default;

describe('...', () => {
  it('ora is mocked', () => {
    const actual = config

    expect(actual).toBeUndefined() // Yes, I realize this is a terrible and incorrect unit test.
  })
})

@skilbjo - You are a life saver. Don't event want to say how many hours I've been wrangling with this.

@skilbjo
Copy link

skilbjo commented Jan 6, 2024

cool, you’re almost there.
add
import { jest } from ‘@jest/globals’

to the top of your config.test.js file, as documented in the ESMAScript modules jest docs page

@mike-weiner
Copy link

@skilbjo

One last follow up for you.

The following results in TypeError: config is not a function:

import {jest} from '@jest/globals'

jest.unstable_mockModule('ora', () => ({
  default: { start: jest.fn(), succeed: jest.fn() },
}));

const config = await import('../config').default;

describe('...', () => {
  it('ora is mocked', () => {
    const actual = config({})

    expect(actual).toBeUndefined() // Yes, I realize this is a terrible and incorrect unit test.
  })
})

However, this works fine:

import {jest} from '@jest/globals'

jest.unstable_mockModule('ora', () => ({
  default: { start: jest.fn(), succeed: jest.fn() },
}));

const config = await import('../config').default;

describe('...', () => {
  it('ora is mocked', () => {
    const actual = config

    expect(actual).toBeUndefined() // Yes, I realize this is a terrible and incorrect unit test.
  })
})

I should be able to call my config function with it's parameters, correct?

@mike-weiner
Copy link

@skilbjo

config/config.js is currently:

import ora from 'ora';

export default (options) => {
  const spinner = !options.raw ? ora('Reading Environment Variables').start() : undefined;
  spinner.succeed(`Environment Variables Retrieved`);
  return true;
};

config/__test__/config.test.js is:

import {jest} from '@jest/globals'

const oraStartMock = jest.fn();
const oraSucceedMock = jest.fn();

jest.unstable_mockModule('ora', () => ({
  default: {
    start: oraStartMock, 
    succeed: oraSucceedMock,
  }
}));

const { default: config } = await import('../config.js');

describe('Mock Ora', () => {
  it('ora is mocked', async () => {
    
    const actual = config({})

    expect(oraStartMock).toHaveBeenCalled();
    expect(actual).toBe(true);
  })
})

I am hitting the following error:

TypeError: ora is not a function

      2 |
      3 | export default (options) => {
    > 4 |   const spinner = !options.raw ? ora('Reading Environment Variables').start() : undefined;
        |                                  ^
      5 |   spinner.succeed(`Environment Variables Retrieved`);
      6 |   return true;
      7 | };

      at ora (config/config.js:4:34)
      at Object.config (config/__tests__/config.test.js:18:20)

@skilbjo
Copy link

skilbjo commented Jan 6, 2024

i think we need requireModule or requireActual ... i'm only just figuring this out myself as well. here's one way to debug:

config/config.js

import ora from 'ora';

export default (options) => {
  console.log({ ora });
  /*
  const spinner = !options.raw ? ora('Reading Environment Variables').start() : undefined;
  spinner.succeed(`Environment Variables Retrieved`);
  return true;
  */
};

run that both in normal mode node config/config.js and with jest npm test -- config/__test__/config.test.js and compare the outputs - that will tell you how you need to mock ora.

but i think what is needed to get it to work for you is something like:

import {jest} from '@jest/globals'

const oraStartMock = jest.fn();
const oraSucceedMock = jest.fn();

jest.unstable_mockModule('ora', () => ({
  ...jest.requireActual('ora'), // <----- ????
  default: {
      ...jest.requireActual('ora'), // <------ ????
    start: oraStartMock, 
    succeed: oraSucceedMock,
  }
}));

const { default: config } = await import('../config.js');

describe('Mock Ora', () => {
  it('ora is mocked', async () => {
    
    const actual = config({})

    expect(oraStartMock).toHaveBeenCalled();
    expect(actual).toBe(true);
  })
})

you can fill me in with what you find, i have just got it working for myself today and am not sure all the workarounds etc yet. do let me know ...

@mx-bernhard
Copy link

The ora-package exports a function that when called with its parameters returns an object that has a succeed and a start function (among others). What you currently return is what the aforementioned function returns instead of the function. It should be something like this:

jest.unstable_mockModule('ora', () => ({
  default: () => ({
    start: oraStartMock, 
    succeed: oraSucceedMock,
  })
}));

But I don't think issues on how to mock specific libraries is something that should be discussed in this issue tracker.

@mike-weiner
Copy link

@mx-bernhard - Yes, my bad. Is there a community page that is better suited for general questions?

@mx-bernhard
Copy link

mx-bernhard commented Jan 7, 2024

@mx-bernhard - Yes, my bad. Is there a community page that is better suited for general questions?

@mike-weiner I don't know anything else other than Discord / SO, as mentioned here: https://jestjs.io/help

@davidjb
Copy link
Contributor

davidjb commented Jan 22, 2024

Is there a way to unmock modules set up with unstable_mockModule?

I've tried jest.resetModules() and jest.restoreAllMocks() with no success, but also re-mocking the same module with another call to unstable_mockModule except with passing the original implementation. Wrapping with jest.isolateModulesAsync(...) isn't possible due to a separate bug (Error: Module cache already has entry puppeteer-core/lib/esm/puppeteer/common/Puppeteer.js. This is a bug in Jest, please report it!) so I've ended up working around the issue with:

const actualModule = await import('something');
jest.unstable_mockModule('something', () => {...});
// Do tests, then "restore" the implementation
const mockedModule = await import('something');
mockedModule.default.mockImplementation(actualModule.default);

@mrazauskas
Copy link
Contributor

Try upgrading Jest. There was an issue with .resetModules(), but it must be fixed. Also consider opening new issue with a minimal reproduction. The problem you are talking about is not related with the OP.

@davidjb
Copy link
Contributor

davidjb commented Jan 23, 2024

@mrazauskas Thanks - I'm currently on the latest v29.7.0 so I'll look at creating a separate issue. I'd commented here because this issue is linked from https://jestjs.io/docs/ecmascript-modules#module-mocking-in-esm, suggesting this issue is where work on jest.unstable_mockModule is taking place.

@akdev5
Copy link

akdev5 commented Feb 1, 2024

I am upgrading the jests using es module.
It is mocking well with unstable_mockModule. but no way to reset or unmock the module.
I have to unmock or reset after each case is tested.

afterEach(async () => {
    jest.resetModules();
  });

I tried to use above code, but getting timeout issue.
Anyone resolved this issue?

@mike-weiner
Copy link

@akdev5 - I don't think you need the async call. As an example, here is what I'm doing to unmock the module: https://github.com/mike-weiner/wlbot/blob/main/tests/commands/weather/current.test.js#L35-L41

@akdev5
Copy link

akdev5 commented Feb 5, 2024

I tried to call without async. but seems not working.

@mike-weiner
Copy link

@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!

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

No branches or pull requests