Імітації класів 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 на конструктор-імітацію та замінює всі його методи на функції-імітації, які завжди повертають undefined
. Виклики методів зберігаються в theAutomaticMock.mock.instances[index].methodName.mock.calls
.
Якщо використовувати стрілкові функції у ваших класах, вони не будуть частиною імітації. 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. Наприклад:
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
beforeEach(() => {
// Очистимо всі екземпляри та виклики конструктору та всіх методів:
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', () => {
// Покажемо, що mockClear() працює:
expect(SoundPlayer).not.toHaveBeenCalled();
const soundPlayerConsumer = new SoundPlayerConsumer();
// Конструктор мав знову бути викликаний:
expect(SoundPlayer).toHaveBeenCalledTimes(1);
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
// mock.instances доступний з автоматичними імітаціями:
const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
expect(mockPlaySoundFile.mock.calls[0][0]).toBe(coolSoundFileName);
// Перевірка, аналогічна вищезазначеній:
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});
Ручні імітації
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.
// Імпортуйте цей іменований export у ваш тестовий файл:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
Import the mock and the mock method shared by all instances:
import SoundPlayer, {mockPlaySoundFile} from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer тепер є конструктором-імітацією
beforeEach(() => {
// Очистимо всі екземпляри та виклики конструктору та всіх методів:
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()
з заводським параметром модуля
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};
});
});
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.
// Зверніть увагу: цей варіант не спрацює
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};
});
// результатом буде ReferenceError
jest.mock('./sound-player', () => {
return mockSoundPlayer;
});
Заміна імітації з використанням mockImplementation()
або mockImplementationOnce()
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();
});
});
Імітації функцій-конструкторів у деталях
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.
Ручна імітація, що сама є класом 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. 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:
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}
Імітація за допомогою заводських параметрів модуля
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.
* Функція заводського модуля повинна повертати функцію
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: () => {}};
};
});
Імітація не може бути стрілковою функцією, тому що в JavaScript виклик new
для стрілкової функції не допускається. 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.)