ES6 Class Mocks
Jest pode ser usado para simular (mock, em inglês) classes ES6 que são importadas para arquivos que você deseja testar.
Classes ES6 são funções de construtor com uma sintaxe mais fácil de usar. Therefore, any mock for an ES6 class must be a function or an actual ES6 class (which is, again, another function). So you can mock them using mock functions.
Um Exemplo de Classe ES6
Usaremos um exemplo inventado de uma classe que reproduz arquivos de som, SoundPlayer
e uma classe de consumidor que usa essa classe, SoundPlayerConsumer
. We'll 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);
}
}
As 4 maneiras de criar uma simulação de uma classe ES6
Simulação automática
Chamando jest.mock('./sound-player')
retorna uma "simulação automática" útil que você pode usar para espionar chamadas para o construtor de classe e todos os seus métodos. Substitui a classe ES6 por um construtor simulado e substitui todos os seus métodos com funções de simulação que sempre retornam undefined
. Method calls are saved in theAutomaticMock.mock.instances[index].methodName.mock.calls
.
Tenha em mente que se você usar arrow functions nas suas classes, elas não serão partes do mock. The reason for that is that arrow functions are not present on the object's prototype, they are merely properties holding a reference to a function.
If you don't need to replace the implementation of the class, this is the easiest option to set up. Por exemplo:
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// Limpa todas as instâncias e chamadas de construtor e todos os métodos:
SoundPlayer.mockClear();
});
it('Nós podemos verificar se o consumidor chamou o construtor de classe', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('Nós podemos verificar se o consumidor chamou um método na instância da classe', () => {
// Mostra que mockClear() está funcionando:
expect(SoundPlayer).not.toHaveBeenCalled();
const soundPlayerConsumer = new SoundPlayerConsumer();
// Construtor deve ter sido chamado novamente:
expect(SoundPlayer).toHaveBeenCalledTimes(1);
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
// mock.instances está disponível com simulações automáticas:
const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
// Equivalente a verificação acima:
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});
Simulação manual
Create a manual mock by saving a mock implementation in the __mocks__
folder. This allows you to specify the implementation, and it can be used across test files.
// __mocks__/sound-player.js
// Import this named export into your test file:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
Importe a simulação e o método simulado compartilhado por todas as instâncias:
// 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(() => {
// Limpa todas as instâncias e chama o construtor e todos os métodos:
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('Nós podemos verificar se o consumidor chamou o construtor da classe', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('Nós podemos verificar se o consumidor chamou um método da instância da classe', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});
jest.mock()
com o parâmetro de "module factory"
Chamar jest.mock(path, moduleFactory)
takes a module factory argument. A module factory is a function that returns the mock.
In order to mock a constructor function, the module factory must return a constructor function. In other words, the module factory must 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};
});
});
A limitation with the factory parameter is that, since calls to jest.mock()
are hoisted to the top of the file, it's not possible to first define a variable and then use it in the factory. An exception is made for variables that start with the word 'mock'. It's up to you to guarantee that they will be initialized on time! For example, the following will throw 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};
});
});
mockImplementation()
or mockImplementationOnce()
Replacing the mock using You can replace all of the above mocks in order to change the implementation, for a single test or all tests, by calling mockImplementation()
on the existing mock.
Calls to jest.mock are hoisted to the top of the code. You can specify a mock later, e.g. in beforeAll()
, by calling mockImplementation()
(or mockImplementationOnce()
) on the existing mock instead of using the factory parameter. This also allows 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()
makes mocks appear more complicated than they really are. This section shows how you can create your own mocks to illustrate how mocking works.
Manual mock that is another ES6 class
If you define an ES6 class using the same filename as the mocked class in the __mocks__
folder, it will serve as the mock. This class will be used in place of the real class. This allows you to inject a test implementation for the class, but does not provide a way to spy on calls.
For the contrived example, the mock might 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');
}
}
Mock using module factory parameter
The module factory function passed to jest.mock(path, moduleFactory)
can be a HOF that returns a function*. This will allow calling new
on the mock. Again, this allows you to inject different behavior for testing, but does not provide a way to spy on calls.
* Module factory function must return a function
In order to mock a constructor function, the module factory must return a constructor function. In other words, the module factory must 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
Note that the mock can't be an arrow function because calling new
on an arrow function is not allowed in JavaScript. 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, unless the code is transpiled to ES5, e.g. by babel-preset-env. (ES5 doesn't have arrow functions nor classes, so both will be transpiled to plain functions.)
Keeping track of usage (spying on the mock)
Injecting a test implementation is helpful, but you will probably also want to test whether the class constructor and methods are called with the correct parameters.
Spying on the constructor
In order to track calls to the constructor, replace the function returned by the HOF with a Jest mock function. Create it with jest.fn()
, and then specify its implementation with mockImplementation()
.
import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});
This will let us inspect usage of our mocked class, using SoundPlayer.mock.calls
: expect(SoundPlayer).toHaveBeenCalled();
or near-equivalent: expect(SoundPlayer.mock.calls.length).toEqual(1);
Mocking non-default class exports
If the class is not the default export from the module then you need to return an object with the key that is the same as the class export name.
import {SoundPlayer} from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return {
SoundPlayer: jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
}),
};
});
Spying on methods of our class
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.
A new object will be created each time the mock constructor function is called during tests. To spy on method calls in all of these objects, we populate playSoundFile
with another mock function, and store a reference to that same mock function in our test file, so 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 would be:
// __mocks__/sound-player.js
// Import this named export into your test file
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
Usage is similar to the module factory function, except that you can omit the second argument from jest.mock()
, and you must import the mocked method into your test file, since it is no longer defined there. Use the original module path for this; don't include __mocks__
.
Cleaning up between tests
To clear the record of calls to the mock constructor function and its methods, we call mockClear()
in the beforeEach()
function:
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
Complete example
Here's a complete test file which uses the module factory parameter to jest.mock
:
// sound-player-consumer.test.js
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
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('We can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
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);
});