Skip to main content
Version: 29.7

ES6 类模拟

Jest 可用于模拟导入到要测试的文件中的 ES6 类。

¥Jest can be used to mock ES6 classes that are imported into files you want to test.

ES6 类是带有一些语法糖的构造函数。因此,ES6 类的任何模拟都必须是一个函数或一个实际的 ES6 类(这又是另一个函数)。所以你可以使用 模拟函数 来模拟它们。

¥ES6 classes are constructor functions with some syntactic sugar. Therefore, any mock for an ES6 class must be a function or an actual ES6 class (which is, again, another function). So you can mock them using mock functions.

ES6 类示例

¥An ES6 Class Example

我们将使用一个播放声音文件的类 SoundPlayer 和使用该类 SoundPlayerConsumer 的消费者类的人为示例。我们将在 SoundPlayerConsumer 的测试中模拟 SoundPlayer

¥We'll use a contrived example of a class that plays sound files, SoundPlayer, and a consumer class which uses that class, SoundPlayerConsumer. We'll mock SoundPlayer in our tests for 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 种方法

¥The 4 ways to create an ES6 class mock

自动模拟

¥Automatic mock

调用 jest.mock('./sound-player') 返回一个有用的 "自动模拟",你可以使用它来监视对类构造函数及其所有方法的调用。它将 ES6 类替换为模拟构造函数,并将其所有方法替换为始终返回 undefined模拟函数。方法调用保存在 theAutomaticMock.mock.instances[index].methodName.mock.calls

¥Calling jest.mock('./sound-player') returns a useful "automatic mock" you can use to spy on calls to the class constructor and all of its methods. It replaces the ES6 class with a mock constructor, and replaces all of its methods with mock functions that always return undefined. Method calls are saved in theAutomaticMock.mock.instances[index].methodName.mock.calls.

注意

如果你在类中使用箭头函数,它们将不会成为模拟的一部分。原因是箭头函数不存在于对象的原型上,它们只是保存函数引用的属性。

¥If you use arrow functions in your classes, they will not be part of the mock. 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. For example:

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);
});

手动模拟

¥Manual mock

通过在 __mocks__ 文件夹中保存模拟实现来创建 手动模拟。这允许你指定实现,并且可以跨测试文件使用。

¥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.

__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;

导入 mock 以及所有实例共享的 mock 方法:

¥Import the mock and the mock method shared by all instances:

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()

¥Calling jest.mock() with the module factory parameter

jest.mock(path, moduleFactory) 采用模块工厂参数。模块工厂是一个返回模拟的函数。

¥jest.mock(path, moduleFactory) takes a module factory argument. A module factory is a function that returns the mock.

为了模拟构造函数,模块工厂必须返回构造函数。换句话说,模块工厂必须是一个返回函数的函数 - 高阶函数(HOF)。

¥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};
});
});
提醒

由于对 jest.mock() 的调用被提升到文件顶部,Jest 会阻止访问超出范围的变量。默认情况下,你不能先定义变量,然后在工厂中使用它。Jest 将对以单词 mock 开头的变量禁用此检查。但是,你仍然需要保证它们按时初始化。请注意 颞死区

¥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.

例如,由于在变量声明中使用 fake 而不是 mock,以下内容将引发超出范围的错误。

¥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};
});
});

尽管在变量声明中使用了 mock,以下内容仍将抛出 ReferenceError,因为 mockSoundPlayer 未封装在箭头函数中,因此在提升后初始化之前进行访问。

¥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;
});

使用 mockImplementation()mockImplementationOnce() 替换模拟

¥Replacing the mock using mockImplementation() or mockImplementationOnce()

你可以通过在现有模拟上调用 mockImplementation() 来替换上述所有模拟,以便更改单个测试或所有测试的实现。

¥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.

对 jest.mock 的调用被提升到代码的顶部。你可以稍后指定一个模拟,例如 在 beforeAll() 中,通过在现有模拟上调用 mockImplementation()(或 mockImplementationOnce())而不是使用工厂参数。如果需要,这还允许你在测试之间更改模拟:

¥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();
});
});

深入:了解模拟构造函数

¥In depth: Understanding mock constructor functions

使用 jest.fn().mockImplementation() 构建构造函数模拟会使模拟看起来比实际情况更复杂。本节展示如何创建自己的模拟来说明模拟的工作原理。

¥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 类

¥Manual mock that is another ES6 class

如果你使用与 __mocks__ 文件夹中的模拟类相同的文件名定义 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:

__mocks__/sound-player.js
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}

playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}

使用模块工厂参数进行模拟

¥Mock using module factory parameter

传递给 jest.mock(path, moduleFactory) 的模块工厂函数可以是返回函数*的 HOF。这将允许在模拟上调用 new。同样,这允许你注入不同的行为进行测试,但不提供监视调用的方法。

¥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.

* 模块工厂函数必须返回一个函数

¥* Module factory function must return a function

为了模拟构造函数,模块工厂必须返回构造函数。换句话说,模块工厂必须是一个返回函数的函数 - 高阶函数(HOF)。

¥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。所以这行不通:

¥The mock can't be an arrow function because calling new on an arrow function is not allowed in JavaScript. So this won't work:

jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});

这将抛出类型错误:_soundPlayer2.default 不是构造函数,除非代码被转译为 ES5,例如 到 @babel/preset-env 为止。(ES5 没有箭头函数也没有类,因此两者都会被转换为普通函数。)

¥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.)

模拟类的特定方法

¥Mocking a specific method of a class

假设你想要模拟或监视类 SoundPlayer 中的方法 playSoundFile。一个简单的例子:

¥Lets say that you want to mock or spy on the method playSoundFile within the class SoundPlayer. A simple example:

// 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 方法

¥Static, getter and setter methods

假设我们的类 SoundPlayer 有一个 getter 方法 foo 和一个静态方法 brand

¥Lets imagine our class SoundPlayer has a getter method foo and a static method 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';
}
}

你可以轻松地模拟/监视它们,这是一个示例:

¥You can mock/spy on them easily, here is an example:

// 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();
});

跟踪使用情况(监视模拟)

¥Keeping track of usage (spying on the mock)

注入测试实现很有帮助,但你可能还想测试是否使用正确的参数调用类构造函数和方法。

¥Injecting a test implementation is helpful, but you will probably also want to test whether the class constructor and methods are called with the correct parameters.

监视构造函数

¥Spying on the constructor

为了跟踪对构造函数的调用,请将 HOF 返回的函数替换为 Jest 模拟函数。使用 jest.fn() 创建它,然后使用 mockImplementation() 指定其实现。

¥In order to track calls to the constructor, replace the function returned by the HOF with a Jest mock function. Create it with jest.fn(), and then specify its implementation with 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);

¥This will let us inspect usage of our mocked class, using SoundPlayer.mock.calls: expect(SoundPlayer).toHaveBeenCalled(); or near-equivalent: expect(SoundPlayer.mock.calls.length).toBeGreaterThan(0);

模拟非默认类导出

¥Mocking non-default class exports

如果该类不是模块的默认导出,那么你需要返回一个具有与类导出名称相同的键的对象。

¥If the class is not the default export from the module then you need to return an object with the key that is the same as the class export name.

import {SoundPlayer} from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return {
SoundPlayer: jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
}),
};
});

监视我们类的方法

¥Spying on methods of our class

我们的模拟类需要提供在测试期间调用的任何成员函数(示例中的 playSoundFile),否则我们将因调用不存在的函数而收到错误。但我们可能还想监视对这些方法的调用,以确保使用预期的参数调用它们。

¥Our mocked class will need to provide any member functions (playSoundFile in the example) that will be called during our tests, or else we'll get an error for calling a function that doesn't exist. But we'll probably want to also spy on calls to those methods, to ensure that they were called with the expected parameters.

测试期间每次调用模拟构造函数时都会创建一个新对象。为了监视所有这些对象中的方法调用,我们用另一个模拟函数填充 playSoundFile,并将对该同一模拟函数的引用存储在我们的测试文件中,以便在测试期间可用。

¥A new object will be created each time the mock constructor function is called during tests. To spy on method calls in all of these objects, we populate playSoundFile with another mock function, and store a reference to that same mock function in our test file, so it's available during tests.

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
});
});

与此等效的手动模拟将是:

¥The manual mock equivalent of this would be:

__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;

用法与模块工厂函数类似,只是你可以省略 jest.mock() 中的第二个参数,并且必须将模拟方法导入到你的测试文件中,因为它不再在那里定义。为此使用原始模块路径;不包括 __mocks__

¥Usage is similar to the module factory function, except that you can omit the second argument from jest.mock(), and you must import the mocked method into your test file, since it is no longer defined there. Use the original module path for this; don't include __mocks__.

测试之间的清理

¥Cleaning up between tests

为了清除模拟构造函数及其方法的调用记录,我们在 beforeEach() 函数中调用 mockClear()

¥To clear the record of calls to the mock constructor function and its methods, we call mockClear() in the beforeEach() function:

beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});

完整示例

¥Complete example

这是一个完整的测试文件,它使用 jest.mock 的模块工厂参数:

¥Here's a complete test file which uses the module factory parameter to jest.mock:

sound-player-consumer.test.js
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);
});