I\'d like to mock my ES6 class imports within my test files.
If the class being mocked has multiple consumers, it may make sense
Updated with a solution thanks to feedback from @SimenB on GitHub.
The factory function must return the mock: the object that takes the place of whatever it's mocking.
Since we're mocking an ES6 class, which is a function with some syntactic sugar, then the mock must itself be a function. Therefore the factory function passed to jest.mock()
must return a function; in other words, it must be a higher-order function.
In the code above, the factory function returns an object. Since calling new
on the object fails, it doesn't work.
new
on:Here's a simple version that, because it returns a function, will allow calling new
:
jest.mock('./sound-player', () => {
return function() {
return { playSoundFile: () => {} };
};
});
Note: Arrow functions won't work
Note that our mock can't be an arrow function because we can't call new on an arrow function in Javascript; that's inherent in the language. So this won't work:
jest.mock('./sound-player', () => {
return () => { // Does not work; arrow functions can't be called with new
return { playSoundFile: () => {} };
};
});
This will throw TypeError: _soundPlayer2.default is not a constructor.
Not throwing errors is all well and good, but we may need to test whether our constructor was called with the correct parameters.
In order to track calls to the constructor, we can replace the function returned by the HOF with a Jest mock function. We create it with jest.fn(), and then we specify its implementation with mockImplementation().
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => { // Works and lets you check for constructor calls
return { playSoundFile: () => {} };
});
});
This will let us inspect usage of our mocked class, using SoundPlayer.mock.calls
.
Our mocked class will need to provide any member functions (playSoundFile
in the example) that will be called during our tests, or else we'll get an error for calling a function that doesn't exist. But we'll probably want to also spy on calls to those methods, to ensure that they were called with the expected parameters.
Because a new mock object will be created during our tests, SoundPlayer.playSoundFile.calls
won't help us. To work around this, we populate playSoundFile
with another mock function, and store a reference to that same mock function in our test file, so we can access it during tests.
let mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => { // Works and lets you check for constructor calls
return { playSoundFile: mockPlaySoundFile }; // Now we can track calls to playSoundFile
});
});
Here's how it looks in the test file:
import SoundPlayerConsumer from './sound-player-consumer';
import SoundPlayer from './sound-player';
let mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return { playSoundFile: mockPlaySoundFile };
});
});
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(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});
For anyone reading this question, I have setup a GitHub repository to test mocking modules and classes. It is based on the principles described in the answer above, but it covers both default and named exports.
If you are still getting TypeError: ...default is not a constructor
and are using TypeScript keep reading.
TypeScript is transpiling your ts file and your module is likely being imported using ES2015s import.
const soundPlayer = require('./sound-player')
.
Therefore creating an instance of the class that was exported as a default will look like this:
new soundPlayer.default()
.
However if you are mocking the class as suggested by the documentation.
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return { playSoundFile: mockPlaySoundFile };
});
});
You will get the same error because soundPlayer.default
does not point to a function.
Your mock has to return an object which has a property default that points to a function.
jest.mock('./sound-player', () => {
return {
default: jest.fn().mockImplementation(() => {
return {
playSoundFile: mockPlaySoundFile
}
})
}
})
For named imports, like import { OAuth2 } from './oauth'
, replace default
with imported module name, OAuth2
in this example:
jest.mock('./oauth', () => {
return {
OAuth2: ... // mock here
}
})