Jest 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);
}
}
创建 ES6 类模拟的 4 种方法
自动模拟
调用jest.mock('./sound-player')
返回一个有用的“自动模拟”,你可以使用它来监视对类构造函数及其所有方法的调用。它取代了ES6类与模拟构造,并将其所有方法始终返回未定义的模拟函数。方法调用保存在theAutomaticMock.mock.instances[index].methodName.mock.calls
.
请注意,如果你在类中使用箭头函数,它们将不会成为模拟的一部分。原因是箭头函数不存在于对象的原型中,它们只是持有对函数的引用的属性。
如果不需要替换类的实现,这是最容易设置的选项。例如:
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);
});
手动模拟
通过在__mocks__
文件夹中保存模拟实现来创建手动模拟。这允许指定实现,并且它可以跨测试文件使用。
// __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;
导入所有实例共享的模拟和模拟方法:
// 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() 使用模块工厂参数调用
jest.mock(path, moduleFactory)
接受一个模块工厂参数。模块工厂是一个返回模拟的函数。
为了模拟构造函数,模块工厂必须返回一个构造函数。换句话说,模块工厂必须是一个返回函数的函数——高阶函数(HOF)。
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
factory 参数的一个限制是,因为调用jest.mock()
被提升到文件的顶部,所以不可能先定义一个变量然后在工厂中使用它。以单词“mock”开头的变量是一个例外。由您来保证它们会按时初始化!例如,由于在变量声明中使用了 'fake' 而不是 'mock',以下代码将抛出一个范围外错误:
// 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() 或替换模拟 mockImplementationOnce()
可以通过对现有的模拟调用mockImplementation()
来替换上述所有模拟,以更改单个测试或所有测试的实现。
对 jest.mock 的调用被提升到代码的顶部。可以稍后在beforeAll()
指定一个模拟,方法时对现有模拟调用mockImplementation()
(或mockImplementationOnce()
), 而不是使用工厂参数。如果需要,这还允许在测试之间更改模拟:
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()
构建构造函数模拟会使模拟看起来比实际更复杂。本节介绍了如何创建自己的模拟,来说明模拟的工作原理。
另一个 ES6 类的手动模拟
如果使用与__mocks__
文件夹中的模拟类相同的文件名定义 ES6 类,它将用作模拟。这个类将用于代替真正的类。这允许你为类注入测试实现,但不提供监视调用的方法。
对于人为的示例,模拟可能如下所示:
// __mocks__/sound-player.js
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
模拟。同样,这允许你测试注入不同的行为,但不提供监视调用的方法。
* 模块工厂函数必须返回一个函数
为了模拟构造函数,模块工厂必须返回一个构造函数。换句话说,模块工厂必须是一个返回函数的函数——高阶函数(HOF)。
jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
注意:箭头函数不起作用
请注意,模拟不能是箭头函数,因为newJavaScript 中不允许调用箭头函数。所以这行不通:
jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});
这将抛出TypeError: _soundPlayer2.default is not a constructor
,除非代码被转换为 ES5,例如通过@babel/preset-env
. (ES5 没有箭头函数和类,所以两者都将被转换为普通函数。)
跟踪使用情况(监视模拟)
注入测试实现很有帮助,但您可能还想测试是否使用正确的参数调用了类构造函数和方法。
监视构造函数
为了跟踪对构造函数的调用,将 HOF 返回的函数替换为 Jest 模拟函数。用 来创建它jest.fn(),然后用 来指定它的实现mockImplementation()
。
import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});
这将让我们检查模拟类的使用情况,使用SoundPlayer.mock.calls
:expect(SoundPlayer).toHaveBeenCalled();
或接近等效的:expect(SoundPlayer.mock.calls.length).toEqual(1);
模拟非默认类导出
如果类不是模块的默认导出,那么您需要返回一个对象,其键与类导出名称相同。
import {SoundPlayer} from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return {
SoundPlayer: jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
}),
};
});
监视我们类的方法
我们的模拟类需要提供playSoundFile
在我们的测试期间将被调用的任何成员函数(在示例中),否则我们将在调用不存在的函数时出错。但是我们可能还想监视对这些方法的调用,以确保使用预期的参数调用它们。
每次在测试期间调用模拟构造函数时,都会创建一个新对象。为了监视所有这些对象中的方法调用,我们填充playSoundFile
了另一个模拟函数,并将对同一个模拟函数的引用存储在我们的测试文件中,以便在测试期间可用。
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
});
});
与此等效的手动模拟将是:
// __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;
用法类似于模块工厂函数,不同之处在于您可以省略 from 的第二个参数jest.mock()
,并且你必须将模拟方法导入到你的测试文件中,因为它不再在那里定义。为此使用原始模块路径;不包括__mocks__
.
测试之间的清理
为了清除对模拟构造函数及其方法的调用记录,我们mockClear()在beforeEach()
函数中调用:
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
完整示例
这是一个完整的测试文件,它使用模块工厂参数来jest.mock
:
// sound-player-consumer.test.js
import SoundPlayerConsumer from './sound-player-consumer';
import SoundPlayer from './sound-player';
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);
});