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

TypeError: _MyClass2.default is not a constructor using jest.mock(path, factory) on ES6 class import #5023

Closed
jonathan-stone opened this issue Dec 6, 2017 · 9 comments

Comments

@jonathan-stone
Copy link
Contributor

jonathan-stone commented Dec 6, 2017

Do you want to request a feature or report a bug?

It's not clear from the docs whether this is expected behavior or not. So at a minimum it's a documentation bug and code feature request.

I'd be happy to submit a PR if this is confirmed to be a real issue.

What is the current behavior?
Assuming the following scenario: An ES6 class (MyClassConsumer) is being tested with Jest. That class imports another ES6 class (MyClass) and calls new MyClass() to create a new instance/object of that class. In the test for MyClassConsumer, MyClass is mocked since that class is not to be tested. See the demo repo for a full example, or see sample code at the bottom of this issue.

When mocking es6 classes using jest.mock('./my-class', ()=>{return {myFunc: jest.fn()}}), the mock does not function correctly. This results in the error TypeError: _MyClass2.default is not a constructor in file MyClassConsumer on the line where it calls new MyClass().

There is nothing that can be passed as the module factory parameter (2nd parameter to jest.mock()) that will correct this error.

There is a workaround, which is to use jest.mock() and then separately call MyClass.mockImplementation(...).

If the current behavior is a bug, please provide the steps to reproduce and
either a repl.it demo through https://repl.it/languages/jest or a minimal
repository on GitHub that we can yarn install and yarn test.

Repo demonstrating the issue is here:
https://github.com/jonathan-stone/jest-es6-classes-demo

Used create-react-app to generate a base React app, and added demo files into src/es6-classes-demo.

What is the expected behavior?

Documentation expected behavior:

The docs specifically mention how to mock ES6 class imports, with at least one example.

Code expected behavior:

Passing a module factory function into jest.mock() allows files that import the mocked class to call new on it without throwing an error.

Please provide your exact Jest configuration and mention your Jest, node,
yarn/npm version and operating system.

OS: MacOS Sierra 10.12.6
Jest version: 21.2.1
Node version: 8.9.0
NPM version: 5.5.1
Jest configuration: Various. Here's an example which is confirmed to repro the issue:

module.exports = {
  verbose: true,
  preset: 'react-native',
  setupFiles: ['./config/jest/setup-tests.js'],
  modulePathIgnorePatterns: ['<rootDir>/src/mocking-factory-tests']
};

In demo repo, Jest config is provided by react-scripts.
Create-react-app version: 1.4.3

(Demo repo uses jest version 20.0.4 since that's what CRA created. But same issue occurs with latest Jest.)

Example code:

Class being mocked - sound-player.js:

export default class SoundPlayer {
  constructor() {
    // Stub
    this.whatever = 'whatever';
  }

  playSoundFile(fileName) {
    // Stub
    console.log('Playing sound file ' + fileName);
  }
}

export {SoundPlayer};


Class being tested - sound-player-consumer.js

import SoundPlayer from './sound-player';

export default class SoundPlayerConsumer {
  constructor() {
    this.soundPlayer = new SoundPlayer();
  }

  playSomethingCool() {
    const coolSoundFileName = 'song.mp3';
    this.soundPlayer.playSoundFile(coolSoundFileName);
  }
}
 

Test that generates the error:

import SoundPlayerConsumer from './sound-player-consumer';
import SoundPlayer from './sound-player';

// These tests all fail when SoundPlayerConsumer calls the constructor of SoundPlayer,
// throwing TypeError: _soundPlayer2.default is not a constructor

jest.mock('./sound-player', () => {
  return {
    playSoundFile: jest.fn()
  };
});

it('The consumer should be able to call new() on SoundPlayer', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(soundPlayerConsumer).toBeTruthy(); // Constructor ran with no errors
});

it('We can check if the consumer called the class constructor', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalled();
});

it('We can check if the consumer called a method on the class instance', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = 'song.mp3';
  soundPlayerConsumer.playSomethingCool();
  expect(SoundPlayer.playSoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});

Test Output
FAIL src/es6-classes-demo/sound-player-consumer-factory-mock.test.js
● The consumer should be able to call new() on SoundPlayer

TypeError: _soundPlayer2.default is not a constructor
  
  at new SoundPlayerConsumer (src/es6-classes-demo/sound-player-consumer.js:5:24)
  at Object.<anonymous>.it (src/es6-classes-demo/sound-player-consumer-factory-mock.test.js:14:31)
      at new Promise (<anonymous>)
      at <anonymous>
  at process._tickCallback (internal/process/next_tick.js:188:7)

● We can check if the consumer called the class constructor

TypeError: _soundPlayer2.default is not a constructor
  
  at new SoundPlayerConsumer (src/es6-classes-demo/sound-player-consumer.js:5:24)
  at Object.<anonymous>.it (src/es6-classes-demo/sound-player-consumer-factory-mock.test.js:19:31)
      at new Promise (<anonymous>)
      at <anonymous>
  at process._tickCallback (internal/process/next_tick.js:188:7)

● We can check if the consumer called a method on the class instance

TypeError: _soundPlayer2.default is not a constructor
  
  at new SoundPlayerConsumer (src/es6-classes-demo/sound-player-consumer.js:5:24)
  at Object.<anonymous>.it (src/es6-classes-demo/sound-player-consumer-factory-mock.test.js:24:31)
      at new Promise (<anonymous>)
      at <anonymous>
  at process._tickCallback (internal/process/next_tick.js:188:7)
@SimenB
Copy link
Member

SimenB commented Dec 14, 2017

EDIT: Ignore last post, this diff makes your tests pass:

diff --git i/src/es6-classes-demo/sound-player-consumer-factory-mock.test.js w/src/es6-classes-demo/sound-player-consumer-factory-mock.test.js
index 6554483..c2c548f 100644
--- i/src/es6-classes-demo/sound-player-consumer-factory-mock.test.js
+++ w/src/es6-classes-demo/sound-player-consumer-factory-mock.test.js
@@ -1,13 +1,14 @@
 import SoundPlayerConsumer from './sound-player-consumer';
 import SoundPlayer from './sound-player';
+let mockPlaySoundFile = jest.fn();
 
 // These tests all fail when SoundPlayerConsumer calls the constructor of SoundPlayer,
 // throwing TypeError: _soundPlayer2.default is not a constructor
 
 jest.mock('./sound-player', () => {
-  return {
-    playSoundFile: jest.fn()
-  };
+  return jest.fn().mockImplementation(() => ({
+      playSoundFile: mockPlaySoundFile
+  }));
 });
 
 it('The consumer should be able to call new() on SoundPlayer', () => {
@@ -24,5 +25,5 @@ it('We can check if the consumer called a method on the class instance', () => {
   const soundPlayerConsumer = new SoundPlayerConsumer();
   const coolSoundFileName = 'song.mp3';
   soundPlayerConsumer.playSomethingCool();
-  expect(SoundPlayer.playSoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
+  expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
 });

@SimenB
Copy link
Member

SimenB commented Dec 14, 2017

PR welcome for docs update!

@jonathan-stone
Copy link
Contributor Author

Thanks Simen! Will try this out and create a docs PR in the next couple of weeks.

@jonathan-stone
Copy link
Contributor Author

@SimenB I tried it and it works. I'd like to understand what's going on before submitting a docs PR. Can you say a few words about why this works, and/or point me to the relevant jest source code?

@SimenB
Copy link
Member

SimenB commented Jan 3, 2018

You returned an object, which you tried to new - that doesn't work. By returning a function (such as a jest mock function) it's possible to new it up.

Does that make sense?

@jonathan-stone
Copy link
Contributor Author

jonathan-stone commented Jan 4, 2018

Ah, I see. Yes, that helps a lot, thanks! So then the factory function must be a HOF. I'll clarify that in the docs PR.

(Edit: working on that PR!)

For anyone reading this before the docs are updated, here's more info on StackOverflow:
https://stackoverflow.com/questions/47402005/jest-mock-how-to-mock-es6-class-default-import-using-factory-parameter/47502477#47502477

@nidkil
Copy link

nidkil commented Nov 22, 2018

For anyone reading this comment, I have setup a GitHub repository to test mocking modules and classes. It is based on the principles described in the Stack Overflow post mentioned above, but it covers both default and named exports.

@nickhallph
Copy link

None of the above worked for me. I'm using React / Redux with ES6, and Jest && Enzyme for testing. In my case, I had to mock a node module.

In the file I'm using, and writing a test for, I'm importing the node modules as default:

import nodeModulePackate from 'nodeModulePackage';

So I needed to mock it as a default since I kept getting the error (0, _blah.default) is not a function..
My solution was to do:

jest.mock('nodeModulePackage', () => jest.fn(() => {}));

In my case, I just needed to override the function and make it return an empty object.

If you need to call a function on that node module, you'll do the following:

jest.mock('nodeModulePackage', () => ({ doSomething: jest.fn(() => return 'foo') }));

Hopefully this helps someone :)

@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 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants