模拟函数通过擦去真正的函数实现,捕获函数调用(调用传参),当使用 new
实例化的时候捕获构造函数,并允许测试时配置返回值,从而更简单地测试代码之间的链接,。
这有两种方法可以模拟函数:要么创建一个模拟函数用于测试代码,要么编写一个手动模拟来覆盖模块依赖项。
使用一个 mock function
想象我们正在测试 forEach
函数的实现,该函数为数组中每个项调用回调函数。
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
为了测试这个函数,我们使用一个 mock 函数,并且检查这个 mock 的状态以确保回调如期望所调用。
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// 这个 mock 函数被调用了两次
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);
// 函数第一个调用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(42);
.mock 属性
所有的 mock 函数都有这个特殊的 .mock
属性,这个属性里面存储了函数如何被调用和函数返回什么的数据。这个.mock
属性也追踪每个调用的this
值,因此也可以检查这个值:
const myMock = jest.fn();
const a = new myMock(); // 使用 new 方法新建实例 a
const b = {};
const bound = myMock.bind(b); // 将 myMock 的上下文绑定到 b
bound();
console.log(myMock.mock.instances);
// > [ <a>, <b> ]
这些 mock 成员在测试中非常有用,可以断言这些函数如何被调用、实例化或返回什么:
// 这个函数实际上被调用了一次
expect(someMockFunction.mock.calls.length).toBe(1);
// 函数第一次调用的第一个参数是 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// 函数第一次调用的第二个参数是 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
// 函数第一次调用的返回值是 ’return value‘
expect(someMockFunction.mock.results[0].value).toBe('return value');
// 这个函数被实例化了两次
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
// 优先使用 mockReturnValueOnce 返回的值,当 mockReturnValueOnce 调用次数结束后,默认返回 mockReturnValue 的值
对应一个持续传递的函数(forEach,filter)来说,在代码里面使用 mock 函数是非常有效的。这样就可以并不用去关注行为,而关注传入的值是否正确。
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(filterTestFn);
console.log(result);
// > [11]
console.log(filterTestFn.mock.calls);
// > [ [11], [12] ]
实际上,大多数实际示例都涉及到获取依赖组件上的模拟函数并对其进行配置,但是技术是相同的。在这些情况下,尽量避免在没有直接测试的函数中实现逻辑。
Mocking 模块
假设我们有一个 class,从我们的 API 拉取用户。这个 class 使用 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));
});
Mock 实现
不过,有些情况超出了指定返回值的能力,这时候替换 mock 函数的实现则非常有用。这个可以使用 jest.fn
或者 mockImplementationOnce
方法 mock 函数实现。
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 可以简化 this,它的形式是.mockReturnThis()
函数。
const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// is the same as
const otherObj = {
myMethod: jest.fn(function() {
return this;
}),
};
Mock 名字
您可以选择为模拟函数提供一个名称,它将在测试错误输出中显示,而不是“jest.fn()”。如果希望能够快速识别模拟函数,并报告测试输出中的错误,请使用此方法。
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');
自定义匹配器
最后,为了更简单地断言如何调用模拟函数,我们为您添加了一些自定义匹配器函数:
// The mock function was called at least once
expect(mockFunc).toBeCalled();
// The mock function was called at least once with the specified args
expect(mockFunc).toBeCalledWith(arg1, arg2);
// The last call to the mock function was called with the specified args
expect(mockFunc).lastCalledWith(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');
参考
mockFn.mock.calls
包含对这个模拟函数所做的所有调用的调用参数的数组。数组中的每个项都是在调用期间传递的参数数组。
例如:一个 mock 函数 f
被调用了两次,第一次使用参数 f('arg1', 'arg2')
,第二次使用参数 f('arg3', 'arg4')
,将有一个 mock.calls
数组如下所示:
[['arg1', 'arg2'], ['arg3', 'arg4']];
mockFn.mock.results
一个数组,包含对这个模拟函数进行的所有调用的结果。这个数组中的每个条目都是一个对象,其中包含一个type
属性和一个value
属性。type
的值如下:
-
'return'
- 指明这个调用完成后正常返回(return)。 -
'throw'
- 指明这个调用完成后抛出(throw)一个值。 -
'incomplete'
- 指明这个调用没有完成。如果您从模拟函数本身或从模拟调用的函数中测试结果,则会发生这种情况。
这个 value
属性包含了一个抛出(throw)或返回(return)的值。value
是 undefined 当 type === 'incomplete'
。
例如:一个 mock 函数 f
被调用了三次,返回 'result1'
,然后抛出一个错误,最后返回 'result2'
,它的 mock.results
数组将如下所示:
[
{
type: 'return',
value: 'result1',
},
{
type: 'throw',
value: {
/* Error instance */
},
},
{
type: 'return',
value: 'result2',
},
];
mockFn.mock.instances
一个数组,其中包含使用new从这个模拟函数实例化的所有对象实例。
例如:一个 mock 函数被实例化了两次,mock.instances
数组如下:
const mockFn = jest.fn();
const a = new mockFn();
const b = new mockFn();
mockFn.mock.instance[0] === a; // true
mockFn.mock.instance[1] === b; // true
mockFn.mockReturnValue(value)
接受一个值,该值将在调用模拟函数时返回。
const mock = jest.fn();
mock.mockReturnValue(42);
mock(); // 42
mock.mockReturnValue(43);
mock(); // 43
mockFn.mockReturnValueOnce(value)
接受一个值,该值将为对模拟函数的一次调用返回。可以被链接(chained)调用,以便对模拟函数的连续调用返回不同的值。当不再使用mockReturnValueOnce
值时,调用将返回mockReturnValue
指定的值。
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call');
// 'first call', 'second call', 'default', 'default'
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
mockFn.mockResolvedValue(value)
下面方法的语法糖函数:
jest.fn().mockImplementation(() => Promise.resolve(value));
在异步测试 mock 异步函数特别有用:
test('async test', async () => {
const asyncMock = jest.fn().mockResolvedValue(43);
await asyncMock(); // 43
});
mockFn.mockResolvedValueOnce(value)
下面方法的语法糖:
jest.fn().mockImplementationOnce(() => Promise.resolve(value));
多次异步测试 resolve 不同的值非常有用:
test('async test', async () => {
const asyncMock = jest
.fn()
.mockResolvedValue('default')
.mockResolvedValueOnce('first call')
.mockResolvedValueOnce('second call');
await asyncMock(); // first call
await asyncMock(); // second call
await asyncMock(); // default
await asyncMock(); // default
});
网友评论