Mocking ES6 Classes in Jest
You can use Jest to mock ES6 classes that are imported into files you want to test.
ES6 classes are constructor functions that has some syntactic sugar. Therefore, any mock for an ES6 class has to be a function or has to be an actual ES6 class (which is, again, another function). So you can mock them using mock functions.
An ES6 Class Example
We'll use a contrived example of a class that will play sound files, SoundPlayer, and a consumer class which will use that class, SoundPlayerConsumer. We will mock SoundPlayer in our tests for SoundPlayerConsumer.
// sound-player.js
```export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
// 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);
}
}
The 4 ways you can create an ES6 class mock
Automatic mock
When you call jest.mock('./sound-player') it will return a useful "automatic mock" that you can use to spy on calls to the class constructor and all of its methods. It will replace the ES6 class with a mock constructor, and will replace all of its methods with mock functions that always return undefined. Method calls will be saved in
theAutomaticMock.mock.instances[index].methodName.mock.calls.
Please note that when you use arrow functions in your classes, they won't be part of the mock. The reason for this is that arrow functions are not present on the object's prototype, they are just properties holding a reference to a function.
If there is no need to replace the implementation of the class, this will be the easiest option to set up. For instance:
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// will Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
});
it('You can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('You can check if the consumer called a method on the class instance', () => {
// Shows that mockClear() is working:
expect(SoundPlayer).not.toHaveBeenCalled();
const soundPlayerConsumer = new SoundPlayerConsumer();
// Constructor should have been called again:
expect(SoundPlayer).toHaveBeenCalledTimes(1);
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
// mock.instances is available with automatic mocks:
const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
// Equivalent to above check:
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});
Manual mock
Start by creating a manual mock by saving a mock implementation in the __mocks__ folder. This will allow you to specify the implementation, and it can be used across test files.
// __mocks__/sound-player.js
// will import this named export into your test file:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
You should import the mock and the mock method that is shared by all instances:
// sound-player-consumer.test.js
import SoundPlayer, {mockPlaySoundFile} from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// will clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('You can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('You can check if the consumer called a method on the class instance', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});
Calling jest.mock() with the module factory parameter
jest.mock(path, moduleFactory) will take a module factory argument. A module factory is a function that will return the mock.
If you want to mock a constructor function, the module factory has to return a constructor function. In other words, the module factory has to be a function that returns a function - a higher-order function (HOF).
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
One limitation with the factory parameter is that, since calls to jest.mock() will be hoisted to the top of the file, it is not possible to first define a variable and then use it in the factory. An exception will be made for variables that start with the word 'mock'. It is up to you to guarantee that they are initialized on time! For instance, the following throws an out-of-scope error due to the use of 'fake' instead of 'mock' in the variable declaration:
// Note: this will fail
import SoundPlayer from './sound-player';
const fakePlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: fakePlaySoundFile};
});
});
Replacing the mock using mockImplementation() or mockImplementationOnce()
In order to change the implementation, you can replace all of the above mocks for a single test or all tests, when you call mockImplementation() on the existing mock. Calls made to jest.mock are hoisted to the top of the code. A mock can be specified later, e.g. in beforeAll(), when you call mockImplementation() (or mockImplementationOnce()) on the existing mock rather than using the factory parameter. This also will allow you to change the mock between tests, if needed:
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player');
describe('When SoundPlayer throws an error', () => {
beforeAll(() => {
SoundPlayer.mockImplementation(() => {
return {
playSoundFile: () => {
throw new Error('Test error');
},
};
});
});
it('Should throw an error when calling playSomethingCool', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
});
});
In depth: Understanding mock constructor functions
Building your constructor function mock using jest.fn().mockImplementation() will make mocks appear more complicated than they really are. This section will show how you can create your own simple mocks to illustrate how mocking works.
Manual mock that is another ES6 class
In the case where you define an ES6 class using the same filename as the mocked class in the __mocks__ folder, it serves as the mock. This class is used in place of the real class. This will allow you to inject a test implementation for the class, but it does not provide a way to spy on calls.
For example, the mock could look like this:
// __mocks__/sound-player.js
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}
Simple mock using module factory parameter
The module factory function that is passed to jest.mock(path, moduleFactory) can be a HOF that will return a function*. This allows you to call new on the mock. Again, this will allow you to inject different behavior for testing, but will not provide a way to spy on calls.
* Module factory function must return a function
In order for you to mock a constructor function, the module factory has to return a constructor function. In other words, the module factory has to be a function that returns a function - a higher-order function (HOF).
jest.mock('./sound-player', () => {
return function() {
return {playSoundFile: () => {}};
};
});
Note: Arrow functions won't work
Because calling new on an arrow function isn't allowed in JavaScript, mock cannot be an arrow function. Hence, this would not work:
jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions cannot be called with new
return {playSoundFile: () => {}};
};
});
This throws TypeError: _soundPlayer2.default is not a constructor, unless the code will be transpiled to ES5, e.g. using @babel/preset-env. (ES5 does not have arrow functions nor classes, so both are transpiled to plain functions.)
Keeping track of usage (spying on the mock)
Injecting a test implementation is always helpful, but you will probably also wish to test whether the methods and class constructor is called with the correct parameters.
Spying on the constructor
If you want to track calls to the constructor, you should replace the function returned by the HOF with a Jest mock function. First you need to create it with jest.fn(), and then you should specify its implementation with mockImplementation().
import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// will work and let you check for constructor calls:
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});
This lets us inspect usage of our mocked class, using SoundPlayer.mock.calls: expect(SoundPlayer).toHaveBeenCalled(); or the near-equivalent: expect(SoundPlayer.mock.calls.length).toEqual(1);
Spying on methods of our class
Our mocked class needs to provide any member functions (playSoundFile in the example) that are called during our tests, or else we will get an error for calling a function that doesn't exist. But we will probably want to also spy on calls to those methods, if you want to ensure that they were called with the expected parameters.
A new object is created each time the mock constructor function is called during tests. If you want to spy on method calls in all of these objects, you should populate playSoundFile with another mock function, and then store a reference to that same mock function in our test file, so that it's available during tests.
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
// Now we can track calls to playSoundFile
});
});
The manual mock equivalent of this is:
// __mocks__/sound-player.js
// will import this named export into your test file
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
Its usage is similar to the module factory function, except that you ma to choose omit the second argument from jest.mock(), and you have to import the mocked method into your test file, as it is no longer defined there. You should use the original module path for this; do not include __mocks__.
Cleaning up between tests
If you want to clear the record of calls to the mock constructor function and its methods, you should call mockClear() in the beforeEach() function:
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
Complete example
A complete test file that uses the module factory parameter to jest.mock:
// sound-player-consumer.test.js
import SoundPlayerConsumer from './sound-player-consumer';
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('The consumer should be able to call new() on SoundPlayer', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
// Ensure constructor created the object:
expect(soundPlayerConsumer).toBeTruthy();
});
it('you can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('you 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);
});
Previous:
Testing Async Functions in Jest.
Next:
Using Jest with Webpack: Configuration and Setup Guide
It will be nice if you may share this link in any developer community or anywhere else, from where other developers may find this content. Thanks.
https://www.w3resource.com/jest/es6-class-mocks.php
- Weekly Trends and Language Statistics
- Weekly Trends and Language Statistics