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

Created on 6 Dec 2017  ·  8Comments  ·  Source: facebook/jest

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

Most helpful comment

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?

All 8 comments

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);
 });

PR welcome for docs update!

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

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

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?

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

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.

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

Was this page helpful?
0 / 5 - 0 ratings