Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 new
实例化时捕获构造函数的实例、允许测试时配置返回值。
有两种方法可以模拟函数:要么在测试代码中创建一个 mock 函数,要么编写一个手动 mock
来覆盖模块依赖。
使用 mock 函数
假设我们要测试函数 forEach
的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数。
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
为了测试此函数,我们可以使用一个 mock 函数,然后检查 mock 函数的状态来确保回调函数如期调用。
const mockCallback = jest.fn();
forEach([0, 1], mockCallback);
// 此模拟函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);
// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// 第二次调用函数时的第一个参数是 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
.mock 属性
All mock functions have this special .mock
property, which is where data about how the function has been called is kept. .mock
属性还追踪每次调用时 this
的值,所以我们同样可以也检视(inspect) this
:
const myMock = jest.fn();
const a = new myMock();
const b = {};
const bound = myMock.bind(b);
bound();
console.log(myMock.mock.instances);
// > [ <a>, <b> ]
These mock members are very useful in tests to assert how these functions get called, or instantiated:
// 这个函数只调用一次
expect(someMockFunction.mock.calls.length).toBe(1);
// 这个函数被第一次调用时的第一个 arg 是 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// 这个函数被第一次调用时的第二个 arg 是 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
// 这个函数被实例化两次
expect(someMockFunction.mock.instances.length).toBe(2);
// 这个函数被第一次实例化返回的对象中,有一个 name 属性,且被设置为了 'test’
expect(someMockFunction.mock.instances[0].name).toEqual('test');
Mock 的返回值
Mock 函数也可以用于在测试期间将测试值注入代码︰
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
在函数连续传递风格(functional continuation-passing style)的代码中时,Mock 函数也非常有效。 Code written in this style helps avoid the need for complicated stubs that recreate 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);
// > [ [11], [12] ]
大多数现实世界例子中,实际是在依赖的组件上配一个模拟函数并配置它,但手法是相同的。 在这些情况下,尽量避免在非真正想要进行测试的任何函数内实现逻辑。
Mock 实现
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
myMockFn((err, val) => console.log(val));
// > true
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
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
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'
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 名称
You can optionally provide a name for your mock functions, which will be displayed instead of "jest.fn()" in test error output. Use this 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');
自定义匹配器
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:
// 这个 mock 函数至少被调用一次
expect(mockFunc).toBeCalled();
// 这个 mock 函数至少被调用一次,而且传入了特定参数
expect(mockFunc).toBeCalledWith(arg1, arg2);
// 这个 mock 函数的最后一次调用传入了特定参数
expect(mockFunc).lastCalledWith(arg1, arg2);
// 所有的 mock 的调用和名称都被写入了快照
expect(mockFunc).toMatchSnapshot();
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. 它还会在名称上断言。
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.mock.getMockName()).toBe('a mock name');
这些只是一部分,有关匹配器的完整列表,请查阅 参考文档。