Skip to main content
Version: 29.7

模拟函数

模拟函数允许你通过擦除函数的实际实现、捕获对函数的调用(以及这些调用中传递的参数)、捕获使用 new 实例化时的构造函数的实例以及允许测试时配置来测试代码之间的链接 返回值。

¥Mock functions allow you to test the links between code by erasing the actual implementation of a function, capturing calls to the function (and the parameters passed in those calls), capturing instances of constructor functions when instantiated with new, and allowing test-time configuration of return values.

模拟函数有两种方法:通过创建一个模拟函数在测试代码中使用,或者编写 manual mock 来覆盖模块依赖。

¥There are two ways to mock functions: Either by creating a mock function to use in test code, or writing a manual mock to override a module dependency.

使用模拟函数

¥Using a mock function

假设我们正在测试函数 forEach 的实现,该函数为提供的数组中的每个项目调用回调。

¥Let's imagine we're testing an implementation of a function forEach, which invokes a callback for each item in a supplied array.

forEach.js
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}

要测试此函数,我们可以使用模拟函数,并检查模拟的状态以确保按预期调用回调。

¥To test this function, we can use a mock function, and inspect the mock's state to ensure the callback is invoked as expected.

forEach.test.js
const forEach = require('./forEach');

const mockCallback = jest.fn(x => 42 + x);

test('forEach mock function', () => {
forEach([0, 1], mockCallback);

// The mock function was called twice
expect(mockCallback.mock.calls).toHaveLength(2);

// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
});

.mock 属性

¥.mock property

所有模拟函数都有这个特殊的 .mock 属性,该属性保存有关函数如何调用以及函数返回内容的数据。.mock 属性还跟踪每次调用的 this 值,因此也可以对此进行检查:

¥All mock functions have this special .mock property, which is where data about how the function has been called and what the function returned is kept. The .mock property also tracks the value of this for each call, so it is possible to inspect this as well:

const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]

const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// > [ <b> ]

这些模拟成员在测试中非常有用,可以断言这些函数如何被调用、实例化或它们返回什么:

¥These mock members are very useful in tests to assert how these functions get called, instantiated, or what they returned:

// The function was called exactly once
expect(someMockFunction.mock.calls).toHaveLength(1);

// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');

// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');

// The function was called with a certain `this` context: the `element` object.
expect(someMockFunction.mock.contexts[0]).toBe(element);

// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);

// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toBe('test');

// The first argument of the last call to the function was 'test'
expect(someMockFunction.mock.lastCall[0]).toBe('test');

模拟返回值

¥Mock Return Values

模拟函数还可以用于在测试期间将测试值注入到代码中:

¥Mock functions can also be used to inject test values into your code during a test:

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

模拟函数在使用函数连续传递风格的代码中也非常有效。以这种风格编写的代码有助于避免使用复杂的存根来重新创建它们所使用的真实组件的行为。

¥Mock functions are also very effective in code that uses a functional continuation-passing style. Code written in this style helps avoid the need for complicated stubs that recreate the behavior of the real component they're standing in for, in favor of injecting values directly into the test right before they're used.

const filterTestFn = jest.fn();

// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter(num => filterTestFn(num));

console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12

大多数现实世界的示例实际上涉及获取依赖组件上的模拟函数并对其进行配置,但技术是相同的。在这些情况下,请尽量避免在任何未直接测试的函数内部实现逻辑的诱惑。

¥Most real-world examples actually involve getting ahold of a mock function on a dependent component and configuring that, but the technique is the same. In these cases, try to avoid the temptation to implement logic inside of any function that's not directly being tested.

模拟模块

¥Mocking Modules

假设我们有一个从 API 获取用户的类。该类使用 axios 调用 API,然后返回包含所有用户的 data 属性:

¥Suppose we have a class that fetches users from our API. The class uses axios to call the API then returns the data attribute which contains all the users:

users.js
import axios from 'axios';

class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}

export default Users;

现在,为了在不实际访问 API 的情况下测试此方法(从而创建缓慢且脆弱的测试),我们可以使用 jest.mock(...) 函数自动模拟 axios 模块。

¥Now, in order to test this method without actually hitting the API (and thus creating slow and fragile tests), we can use the jest.mock(...) function to automatically mock the axios module.

一旦我们模拟了模块,我们就可以为 .get 提供 mockResolvedValue,它返回我们希望测试断言的数据。实际上,我们是说我们希望 axios.get('/users.json') 返回一个假响应。

¥Once we mock the module we can provide a mockResolvedValue for .get that returns the data we want our test to assert against. In effect, we are saying that we want axios.get('/users.json') to return a fake response.

users.test.js
import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);

// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))

return Users.all().then(data => expect(data).toEqual(users));
});

模拟部分

¥Mocking Partials

模块的子集可以被模拟,模块的其余部分可以保留其实际实现:

¥Subsets of a module can be mocked and the rest of the module can keep their actual implementation:

foo-bar-baz.js
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';

jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz');

//Mock the default export and named export 'foo'
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});

test('should do a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();

expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});

模拟实现

¥Mock Implementations

尽管如此,在某些情况下,超越指定返回值的能力并全面替换模拟函数的实现是有用的。这可以通过模拟函数上的 jest.fnmockImplementationOnce 方法来完成。

¥Still, there are cases where it's useful to go beyond the ability to specify return values and full-on replace the implementation of a mock function. This can be done with jest.fn or the mockImplementationOnce method on mock functions.

const myMockFn = jest.fn(cb => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

当你需要定义从另一个模块创建的模拟函数的默认实现时,mockImplementation 方法非常有用:

¥The mockImplementation method is useful when you need to define the default implementation of a mock function that is created from another module:

foo.js
module.exports = function () {
// some implementation;
};
test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42

当你需要重新创建模拟函数的复杂行为以使多个函数调用产生不同的结果时,请使用 mockImplementationOnce 方法:

¥When you need to recreate a complex behavior of a mock function such that multiple function calls produce different results, use the mockImplementationOnce method:

const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));

myMockFn((err, val) => console.log(val));
// > true

myMockFn((err, val) => console.log(val));
// > false

当模拟函数用完 mockImplementationOnce 定义的实现时,它将执行 jest.fn 定义的默认实现集(如果已定义):

¥When the mocked function runs out of implementations defined with mockImplementationOnce, it will execute the default implementation set with jest.fn (if it is defined):

const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

对于通常是链式方法(因此总是需要返回 this)的情况,我们有一个糖化 API 来以 .mockReturnThis() 函数的形式简化它,该函数也位于所有模拟上:

¥For cases where we have methods that are typically chained (and thus always need to return this), we have a sugary API to simplify this in the form of a .mockReturnThis() function that also sits on all mocks:

const myObj = {
myMethod: jest.fn().mockReturnThis(),
};

// is the same as

const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};

模拟名称

¥Mock Names

你可以选择为模拟函数提供一个名称,该名称将在测试错误输出中显示,而不是 'jest.fn()'。如果你希望能够快速识别在测试输出中报告错误的模拟函数,请使用 .mockName()

¥You can optionally provide a name for your mock functions, which will be displayed instead of 'jest.fn()' in the test error output. Use .mockName() if you want to be able to quickly identify the mock function reporting an error in your test output.

const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');

自定义匹配器

¥Custom Matchers

最后,为了降低断言模拟函数调用方式的要求,我们为你添加了一些自定义匹配器函数:

¥Finally, in order to make it less demanding to assert how mock functions have been called, we've added some custom matcher functions for you:

// The mock function was called at least once
expect(mockFunc).toHaveBeenCalled();

// The mock function was called at least once with the specified args
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);

// The last call to the mock function was called with the specified args
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);

// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();

这些匹配器是检查 .mock 属性的常见形式的糖。如果这更符合你的口味或者你需要执行更具体的操作,你始终可以自己手动执行此操作:

¥These matchers are sugar for common forms of inspecting the .mock property. You can always do this manually yourself if that's more to your taste or if you need to do something more specific:

// The mock function was called at least once
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);

// The mock function was called at least once with the specified args
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);

// The last call to the mock function was called with the specified args
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
arg1,
arg2,
]);

// The first arg of the last call to the mock function was `42`
// (note that there is no sugar helper for this specific of an assertion)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);

// A snapshot will check that a mock was invoked the same number of times,
// in the same order, with the same arguments. It will also assert on the name.
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe('a mock name');

有关匹配器的完整列表,请查看 参考文档

¥For a complete list of matchers, check out the reference docs.