ES6 类模拟
Jest 可用于模拟导入到要测试的文件中的 ES6 类。
ES6 类是带有一些语法糖的构造函数。因此,ES6 类的任何模拟都必须是函数或实际的 ES6 类(再次,是另一个函数)。因此,您可以使用 模拟函数 来模拟它们。
ES6 类示例
我们将使用一个虚构的播放声音文件的类 SoundPlayer
和一个使用该类的消费者类 SoundPlayerConsumer
的示例。我们将在 SoundPlayerConsumer
的测试中模拟 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);
}
}
创建 ES6 类模拟的 4 种方法
自动模拟
调用 jest.mock('./sound-player')
返回一个有用的“自动模拟”,您可以使用它来监视对类构造函数及其所有方法的调用。它用模拟构造函数替换 ES6 类,并将所有方法替换为 模拟函数,这些函数始终返回 undefined
。方法调用保存在 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]).toBe(coolSoundFileName);
// Equivalent to above check:
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});
手动模拟
通过在 __mocks__
文件夹中保存模拟实现来创建 手动模拟。这允许您指定实现,并且它可以在测试文件中使用。
// 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 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};
});
});
由于对 jest.mock()
的调用被提升到文件顶部,Jest 阻止访问超出范围的变量。默认情况下,您不能先定义一个变量,然后在工厂中使用它。Jest 将为以单词 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};
});
});
以下代码尽管在变量声明中使用了 mock
,但仍会抛出 ReferenceError
,因为 mockSoundPlayer
没有包装在箭头函数中,因此在提升后在初始化之前被访问。
import SoundPlayer from './sound-player';
const mockSoundPlayer = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
// results in a ReferenceError
jest.mock('./sound-player', () => {
return mockSoundPlayer;
});
使用 mockImplementation()
或 mockImplementationOnce()
替换模拟
您可以通过在现有模拟上调用 mockImplementation()
来替换所有上述模拟,以便更改实现,用于单个测试或所有测试。
对 jest.mock 的调用被提升到代码的顶部。您可以通过在现有模拟上调用 mockImplementation()
(或 mockImplementationOnce()
)来指定模拟,例如在 beforeAll()
中,而不是使用工厂参数。如果需要,这也允许您在测试之间更改模拟
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 类,它将用作模拟。此类将用于代替真实类。这允许您为类注入测试实现,但没有提供监视调用的方法。
对于虚构的示例,模拟可能如下所示
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: () => {}};
};
});
模拟不能是箭头函数,因为在箭头函数上调用 new
在 JavaScript 中是不允许的。所以这将不起作用
jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});
这将抛出TypeError: _soundPlayer2.default 不是构造函数,除非代码被转译为 ES5,例如由 @babel/preset-env
转译。(ES5 没有箭头函数或类,因此两者都将被转译为普通函数。)
模拟类的特定方法
假设您想模拟或监视类 SoundPlayer
中的方法 playSoundFile
。一个简单的例子
// your jest test file below
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
const playSoundFileMock = jest
.spyOn(SoundPlayer.prototype, 'playSoundFile')
.mockImplementation(() => {
console.log('mocked function');
}); // comment this line if just want to "spy"
it('player consumer plays music', () => {
const player = new SoundPlayerConsumer();
player.playSomethingCool();
expect(playSoundFileMock).toHaveBeenCalled();
});
静态、getter 和 setter 方法
假设我们的类 SoundPlayer
有一个 getter 方法 foo
和一个静态方法 brand
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
get foo() {
return 'bar';
}
static brand() {
return 'player-brand';
}
}
您可以轻松地模拟/监视它们,这是一个示例
// your jest test file below
import SoundPlayer from './sound-player';
const staticMethodMock = jest
.spyOn(SoundPlayer, 'brand')
.mockImplementation(() => 'some-mocked-brand');
const getterMethodMock = jest
.spyOn(SoundPlayer.prototype, 'foo', 'get')
.mockImplementation(() => 'some-mocked-result');
it('custom methods are called', () => {
const player = new SoundPlayer();
const foo = player.foo;
const brand = SoundPlayer.brand();
expect(staticMethodMock).toHaveBeenCalled();
expect(getterMethodMock).toHaveBeenCalled();
});
跟踪使用情况(监视模拟)
注入测试实现很有帮助,但您可能还想测试类构造函数和方法是否使用正确的参数调用。
监视构造函数
为了跟踪对构造函数的调用,用 Jest 模拟函数替换 HOF 返回的函数。使用 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).toBeGreaterThan(0);
模拟非默认类导出
如果类不是模块的默认导出,那么您需要返回一个键与类导出名称相同的对象。
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
});
});
手动模拟的等效代码将是
// Import this named export into your test file
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
用法类似于模块工厂函数,除了您可以省略 jest.mock()
的第二个参数,并且您必须将模拟方法导入到测试文件中,因为它不再在那里定义。为此,请使用原始模块路径;不要包含 __mocks__
。
在测试之间清理
要清除对模拟构造函数及其方法的调用记录,我们在 beforeEach()
函数中调用 mockClear()
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
完整示例
这是一个使用模块工厂参数的完整测试文件,用于 jest.mock
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]).toBe(coolSoundFileName);
});