Jest ES6 类模拟
Jest 可用于模拟导入到要测试的文件中的 ES6 类。
ES6 类是带有一些语法糖的构造函数。因此,任何 ES6 类的模拟都必须是一个函数或一个实际的 ES6 类(这又是另一个函数)。所以你可以使用模拟函数来模拟它们。
ES6 类示例
我们将使用一个播放声音文件的类的人为示例,SoundPlayer,以及使用该类的使用者类SoundPlayerConsumer。我们将SoundPlayer在我们的测试中模拟SoundPlayerConsumer.
// sound-player.jsexport default class SoundPlayer {constructor() {this.foo = 'bar';}playSoundFile(fileName) {console.log('Playing sound file ' + fileName);}}
// sound-player-consumer.jsimport 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 constructorbeforeEach(() => {// 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.jsimport SoundPlayer, {mockPlaySoundFile} from './sound-player';import SoundPlayerConsumer from './sound-player-consumer';jest.mock('./sound-player'); // SoundPlayer is now a mock constructorbeforeEach(() => {// 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 failimport 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.jsexport 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 newreturn {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 fileexport 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.jsimport 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);});