模拟函数
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
模拟函数让您能够通过以下方式测试代码间的关联:抹去函数的实际实现、捕获对该函数的调用(及调用时传递的参数)、捕获通过new实例化的构造函数实例,并允许在测试时配置返回值。
有两种方式创建模拟函数:在测试代码中直接创建模拟函数,或编写manual mock来覆写模块依赖。
使用模拟函数
假设我们正在测试一个forEach函数的实现,该函数会对给定数组中的每个元素调用回调函数。
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}
要测试该函数,我们可以使用模拟函数并检查模拟状态,以验证回调函数是否按预期被调用。
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
对于采用函数式延续传递风格(continuation-passing style)的代码,模拟函数同样非常高效。这种编码风格有助于避免创建复杂的存根(stubs)来模拟真实组件的行为,而是支持在即将使用时直接将值注入测试。
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属性:
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')返回伪造的响应。
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));
});
局部模拟
可以对模块的子集进行模拟,同时保留其余部分的实际实现:
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方法非常有用:
module.exports = function () {
// some implementation;
};
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 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');
完整匹配器列表请查阅参考文档。