Имитаторы классов ES6
Jest может использоваться для создания имитаторов классов ES6, импортируемых в файлы, которые вы желаете протестировать.
Классы ES6 являются функциями-конструкторами с некоторым синтаксическим сахаром. Поэтому любой объект-имитатор для класса ES6 должен быть функцией или фактическим классом ES6 (который, в свою очередь, также является функцией). Таким образом, вы можете имитировать поведение, используя функции создания имитаторов.
Пример класса ES6
Мы будем использовать пример класса, который воспроизводит звуковые файлы, Класс soundplayer
, и потребительский класс, который использует этот класс, SoundPlayerConsumer
. Мы создадим имитатор для SoundPlayer
в наших тестах для 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);
}
}
4 способа создания имитатора класса ES6
Автоматический имитатор
Зовущий jest.mock('/ sound-player')
возвращает полезный "автоматический макет", который можно использовать для отслеживания вызовов конструктора класса и всех его методов. Он заменит класс ES6 на конструктор имитатора, а также заменит все его методы mock-функциями, всегда возвращающими undefined
. Method calls are saved in theAutomaticMock.mock.instances[index].methodName.mock.calls
.
If you use arrow functions in your classes, they will not be part of the 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.
Если вам не нужно заменять реализацию класса, это самый простой вариант для настройки. Например:
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);
});
Создаваемые вручную имитаторы
Create a manual mock by saving a mock implementation in the __mocks__
folder. Это позволяет указать реализацию и использовать ее в тестовых файлах.
// Import this named export into your test file:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
Импортируйте макет и метод макета, совместно используемые всеми экземплярами:
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);
});
Calling jest.mock()
with the module factory parameter
jest.mock(path, moduleFactory)
takes a module factory argument. A module factory is a function that returns the mock.
Чтобы издеваться над функцией конструктора, фабрика модулей должна возвращать функцию конструктора. Другими словами, фабрика модулей должна быть функцией, которая возвращает функцию-функцию более высокого порядка (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;
});
Replacing the mock using mockImplementation()
or mockImplementationOnce()
Вы можете заменить все перечисленное высмеивается для того, чтобы изменить реализацию, на один тест или тесты, по телефону >mockImplementation()
на существующем макете.
Вызывает шутку.макеты поднимаются наверх кода. 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. Это также позволяет при необходимости изменить макет между тестами:
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();
});
});
В глубину: Понимание функций создания имитаторов
Построение макета функции конструктора с помощью Эст.сноска.)(mockImplementation()
делает насмешки более сложными, чем они есть на самом деле. This section shows how you can create your own mocks to illustrate how mocking works.