Simulação de Classes ES6
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. Portanto, qualquer simulação de uma classe ES6 deve ser uma função ou uma classe ES6 real (que é, novamente, outra função). Então, você pode simulá-las usando funções de simulação.
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
. Nós iremos simular SoundPlayer
nos nossos testes para SoundPlayerConsumer
.
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
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
. As chamadas dos métodos são salvas em theAutomaticMock.mock.instances[index].methodName.mock.calls
.
If you use arrow functions in your classes, they will not be part of the mock. Isto ocorre porque as arrow functions não estão presentes no prototype do objeto, elas são apenas propriedades que contêm uma referência para uma função.
Se você não precisa substituir a implementação da classe, esta é a opção mais fácil de configurar. Por exemplo:
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
});
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', () => {
// Show 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]).toBe(coolSoundFileName);
// Equivalent to above check:
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});
Simulação manual
Crie uma simulação manual salvando uma implementação de simulação na pasta __mocks__
. Isso permite que você especifique a implementação, e pode ser usada em arquivos de teste.
// 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:
import SoundPlayer, {mockPlaySoundFile} from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
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).toHaveBeenCalledWith(coolSoundFileName);
});
Chamar jest.mock()
com o parâmetro de "module factory"
jest.mock(path, moduleFactory)
recebe um argumento "module factory". Um "module factory" é uma função que retorna uma simulação.
Para simular uma função de construtor, o "module factory" deve retornar uma função de construtor. Em outras palavras, o "module factory" deve ser uma função que retorna uma função - uma função de ordem superior, também chamada de "higher-order function" (HOF).
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
Since calls to jest.mock()
are hoisted to the top of the file, Jest prevents access to out-of-scope variables. By default, you cannot first define a variable and then use it in the factory. Jest will disable this check for variables that start with the word mock
. However, it is still up to you to guarantee that they will be initialized on time. Be aware of Temporal Dead Zone.
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};
});
});
The following will throw a ReferenceError
despite using mock
in the variable declaration, as the mockSoundPlayer
is not wrapped in an arrow function and thus accessed before initialization after hoisting.
import SoundPlayer from './sound-player';
const mockSoundPlayer = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
// results in a ReferenceError
jest.mock('./sound-player', () => {
return mockSoundPlayer;
});
Substituir a simulação (mock, em inglês) usando mockImplementation()
ou mockImplementationOnce()
É possível substituir todas as simulações acima para alterar a implementação, para um único teste ou todos os testes, chamando mockImplementation()
na simulação existente.
Chamadas para jest.mock são "hoisted" para o topo do código. Você pode especificar uma simulação depois, por exemplo, em beforeAll()
, chamando mockImplementation()
(ou mockImplementationOnce()
) na simulação existente em vez de usar o parâmetro "factory". Isso também permite que você altere a simulação entre os testes, se necessário:
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();
});
});
Em profundidade: Compreensão das funções do construtor das simulações (mock, em inglês)
Construir sua simulação de função de construtor usando jest.fn().mockImplementation()
faz com que as simulações pareçam mais complicadas do que realmente são. Esta seção mostra como você pode criar suas próprias simulações para ilustrar como funciona o simulador.
Simulação manual que é outra classe ES6
Se você definir uma classe ES6 usando o mesmo nome de arquivo que a classe simulada na pasta __mocks__
, ela servirá como a simulação desta classe. Esta classe será usada no lugar da classe verdadeira. Isso permite que você injete uma implementação de teste para a classe, mas não fornece uma maneira de espiar as chamadas.
Para o exemplo inventado, a simulação pode ser assim:
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}
Simular (mock, em inglês) usando parâmetro de "module factory"
A função "module factory" passada para jest.mock(path, moduleFactory)
pode ser uma HOF que retorna uma função*. Isso permitirá chamar new
na simulação. Novamente, isso permite que você injete um comportamento diferente para testes, mas não fornece uma maneira de espiar as chamadas.
* Função "module factory' deve retornar uma função
Para simular uma função de construtor, o "module factory" deve retornar uma função de construtor. Em outras palavras, o "module factory" deve ser uma função que retorna uma função - uma função de ordem superior, também chamada de "higher-order function" (HOF).
jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
The mock can't be an arrow function because calling new
on an arrow function is not allowed in JavaScript. Isso não vai funcionar:
jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});
Isto lançará TypeError: _soundPlayer2.default is not a constructor, a menos que o código seja transpilado para ES5, por exemplo, por @babel/preset-env
. (ES5 não tem "arrow functions" nem classes, então ambas serão transpiladas para funções simples.)
Mocking a specific method of a class
Lets say that you want to mock or spy on the method playSoundFile
within the class SoundPlayer
. A simple example:
// your jest test file below
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
const playSoundFileMock = jest
.spyOn(SoundPlayer.prototype, 'playSoundFile')
.mockImplementation(() => {
console.log('mocked function');
}); // comment this line if just want to "spy"
it('player consumer plays music', () => {
const player = new SoundPlayerConsumer();
player.playSomethingCool();
expect(playSoundFileMock).toHaveBeenCalled();
});
Static, getter and setter methods
Lets imagine our class SoundPlayer
has a getter method foo
and a static method brand
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
get foo() {
return 'bar';
}
static brand() {
return 'player-brand';
}
}
You can mock/spy on them easily, here is an example:
// your jest test file below
import SoundPlayer from './sound-player';
const staticMethodMock = jest
.spyOn(SoundPlayer, 'brand')
.mockImplementation(() => 'some-mocked-brand');
const getterMethodMock = jest
.spyOn(SoundPlayer.prototype, 'foo', 'get')
.mockImplementation(() => 'some-mocked-result');
it('custom methods are called', () => {
const player = new SoundPlayer();
const foo = player.foo;
const brand = SoundPlayer.brand();
expect(staticMethodMock).toHaveBeenCalled();
expect(getterMethodMock).toHaveBeenCalled();
});
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).toBeGreaterThan(0);
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:
// 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
:
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]).toBe(coolSoundFileName);
});