跳至主要内容
版本:29.7

模拟函数

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

有两种方法可以模拟函数:要么创建模拟函数以在测试代码中使用,要么编写手动模拟以覆盖模块依赖项。

使用模拟函数

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

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

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

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属性,该属性用于保存有关函数如何被调用以及函数返回什么的数据。.mock属性还跟踪每次调用的this值,因此也可以检查它。

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> ]

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

// 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');

模拟返回值

模拟函数还可用于在测试期间将测试值注入您的代码。

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

模拟函数在使用函数式延续传递风格的代码中也非常有效。以这种风格编写的代码有助于避免需要复杂的存根,这些存根会重新创建它们所代表的真实组件的行为,而是直接将值注入到测试中,就在它们被使用之前。

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

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

模拟模块

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

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 模块。

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

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

模拟部分

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

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

模拟实现

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

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

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

mockImplementation方法在您需要定义从另一个模块创建的模拟函数的默认实现时很有用。

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

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设置的默认实现(如果已定义)。

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()函数的形式存在于所有模拟中。

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

// is the same as

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

模拟名称

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

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

自定义匹配器

最后,为了减少断言模拟函数如何被调用的要求,我们为您添加了一些自定义匹配器函数。

// 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属性的常见形式的简化。如果您更喜欢这样做,或者您需要做一些更具体的事情,您始终可以手动执行此操作。

// 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');

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