Имитаторы классов ES6
Jest может использоваться для создания имитаторов классов ES6, импортируемых в файлы, которые вы желаете протестировать.
Классы ES6 являются функциями-конструкторами с некоторым синтаксическим сахаром. Поэтому любой объект-имитатор для класса ES6 должен быть функцией или фактическим классом ES6 (который, в свою очередь, также является функцией). Таким образом, вы можете имитировать поведение, используя функции создания имитаторов.
Пример класса ES6
Мы будем использовать пример класса, который воспроизводит звуковые файлы, Класс soundplayer
, и класс-потребитель, который использует этот класс, SoundPlayerConsumer
. Мы создадим имитатор для SoundPlayer
в наших тестах для 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);
}
}
4 способа создания имитатора класса ES6
Автоматический имитатор
Вызов jest.mock('./sound-player')
возвращает полезный "автоматический имитатор", который можно использовать для отслеживания вызовов конструктора класса и всех его методов. Он заменит класс ES6 на конструктор имитатора, а также заменит все его методы mock-функциями, всегда возвращающими undefined
. Method calls are saved in theAutomaticMock.mock.instances[index].methodName.mock.calls
.
Please note that 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]).toEqual(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. Это позволяет указать реализацию и использовать ее в тестовых файлах.
// __mocks__/sound-player.js
// Импортируйте этот именованный объект в свой файл с тестом:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
Импортируйте макет и метод макета, совместно используемые всеми экземплярами:
// 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(() => {
// 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);
});
jest.mock()
with the module factory parameter
Calling 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};
});
});
Ограничение с параметром factory таково, так как вызовы шутка.издеваться()
поднимаются к верхней части файла, невозможно сначала определить переменную, а затем использовать ее в Фабрике. Исключение составляют переменные, которые начинаются со слова "полка". Это до вас, чтобы гарантировать, что они будут инициализированы вовремя! 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 Вы можете заменить все перечисленное высмеивается для того, чтобы изменить реализацию, на один тест или тесты, по телефону >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.
Ручной макет, который является другим классом ES6
If you define an ES6 class using the same filename as the mocked class in the __mocks__
folder, it will serve as the mock. Этот класс будет использоваться вместо реального класса. Это позволяет внедрить реализацию теста для класса, но не предоставляет способ шпионить за вызовами.
Для надуманного примера макет может выглядеть следующим образом:
// __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
Переданная функция фабрики модуля к шутка.макет (путь, moduleFactory)
может быть HOF, который возвращает функцию*. 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
Чтобы издеваться над функцией конструктора, фабрика модулей должна возвращать функцию конструктора. Другими словами, фабрика модулей должна быть функцией, которая возвращает функцию-функцию более высокого порядка (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);
});