ES6 类模拟
Jest 可用于模拟你从文件导入的想要测试的ES6类。
ES6类是带有一些语法糖的构造函数。 因此,ES6类的任何 模拟都必须是一个函数或者一个真实的 ES6类(又是另一个函数)。 所以你可以通过 mock functions 模拟它们。
一个ES6类的例子
我们会举一个例子,在这个例子里面包含一个类 SoundPlayer
用来播放音频文件, 以及另外一个类 SoundPlayerConsumer
, 它将会用到 SoundPlayer。 我们会在 SoundPlayerConsumer
的 test 文件里面模拟 SoundPlayer
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-payer')
会得到一个"自动模拟", 你可以监听这个模拟上 constructor 的调用以及它所有方法的调用。 它将会使用一个模拟的 constructor 替换原先的 Es6 类,以及使用返回 undefined 的[模拟函数](MockFunctions. md)替换掉这个类上所有的方法。 这些方法的调用会保存在 theAutomaticMock.mock.instances[index].methodName.mock.calls
。
If you use arrow functions in your classes, they will not be part of the mock. 这样做的原因是箭头函数不能代表一个对象的原型,它们仅仅是是一个函数的引用。
如果你不需要替换class的实现,这是最简单的设置方式。 例如:
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);
});
手动模拟
通过保存一份 模拟的的实现__mocks__
文件夹,你可以创建一个手动模拟。 这使得你可以指定实现,这个实现将被test文件使用。
// 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 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 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()
同时传入模块工厂参数
jest.mock(path, moduleFactory)
能接收 模块工厂 参数。 模块工厂是一个函数,这个函数会返回 mock。
为了模拟 constructor 构造函数,模块工厂必须返回一个构造函数。 也就是说,模块工厂必须返回一个函数,而这个函数会返回另外一个函数 - 高阶函数(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()
对一个或者多个test,通过在已经存在的 mock 上调用mockImplementation()
, 你可以更改所有 mock 的实现。
Jest.mock 的调用会被提升到文件顶部。 通过在已经存在的 mock 上调用mockImplementation()
(或者 mockImplementationOnce()
),这可以让你延迟指定它的实现,比如在beforeAll()
里面,而不是之前提到过的使用工厂函数。 这能让你在你需要时,可以在不同 test 之间改变mock的实现。
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();
});
});
深入:理解模拟的构造函数
使用 jest.fn().mockImplementation()
去打造你的构造函数的 mock 将使得这些 mock 变得比它们自身更复杂。 这一章节会给你展示如何创建你自己的模拟,这会阐述模拟是如何工作的。
手工模拟是另外一个ES6类
如果你在__mocks__
文件夹下的一个与你 mock 类同名的文件里定义一个 ES6 类,这个类将会被当成 mock 使用。 这个类将会被取代真实的类。 这将使得你可以为这个类注入一个 test 实现,但是这样做不能让你监测它的调用。
对于我们的例子来讲,这个 mock 也许会像这有:
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}
使用模块工厂函数进行模拟
传入jest.mock(path, moduleFactory)
的模块工厂函数可以是一个高阶函数HOF,它将会返回另外一个函数。 这能使得你的模拟能被 new
调用。 同样的,这种方式让你为测试注入不同的实现,但是不能让你去监测调用。