模拟函数或间谍揭秘-jest.fn() 如何工作?
介绍
模拟函数(也称为间谍)是一种特殊函数,它允许我们跟踪外部代码如何调用特定函数。我们不仅可以测试函数的输出,还可以获得有关函数如何使用的其他信息。
通过使用mock函数,我们可以知道以下内容:
- 接到的电话数量。
- 每次调用时使用的参数值。
- 每次调用时的“上下文”或此值。
- 函数如何退出以及产生了哪些值。
我们还可以提供一种实现来覆盖原始函数行为。并且我们可以描述特定的返回值以适合我们的测试。
函数是一等公民
在 JavaScript 中,函数可以像任何值一样处理。你可以将它们作为参数传递给其他函数,可以将它们分配为对象的属性(作为方法),也可以从它们返回其他函数。从内部来看,函数只是可以调用的特殊对象。
让我们看一些例子:
function greet(name) {
return `Hello ${name}!`;
}
// Functions can be assigned to variables:
const other = fn;
// `other` and `fn` referr to the same function object:
other === fn; // true
// Can be passed as argument values:
function greetWorld(greettingFn) {
return greetingFn('world');
}
greetWorld(greet); // Hello world!
高阶函数
高阶函数是可以对其他函数进行操作的函数。要么接收它们作为参数,要么返回它们作为值。在前面的例子中,我们可以说greetWorld是一个高阶函数,因为它需要一个函数作为输入参数。
在 JavaScript 中,有很多地方都有高阶函数。Array.prototype方法就是很好的例子。它们接收一个回调函数,该函数在数组对象的所有元素上调用。
何时使用模拟函数
当我们想要替换特定函数的返回值时,或者当我们想要检查测试对象是否以某种方式执行函数时,我们可以使用模拟函数。我们可以模拟独立函数或外部模块方法,并且可以提供特定的实现。例如,假设您正在测试使用另一个模块向外部 API 发出请求的业务逻辑模块。然后,您可以模拟依赖项的函数,以避免在测试中触及 API。然后,您可以运行测试,了解模拟函数将返回给测试对象的内容。
我们能够为外部依赖项提供实现,这一点很有用,因为它允许我们隔离测试对象。我们可以专注于它。单元测试将完全专注于业务逻辑,而无需关心外部 API。
此外,当我们实现高阶函数时,我们可以测试测试对象如何使用其他函数。我们将模拟传递给我们要测试的函数,然后我们就可以验证它是如何使用的。
如何使用 jest.fn
有几种方法可以创建模拟函数。jest.fn方法允许我们直接创建一个新的模拟函数。如果你正在模拟一个对象方法,你可以使用jest.spyOn。如果你想模拟整个模块,你可以使用jest.mock。
在本指南中,我们将重点介绍jest.fn方法,这是创建模拟函数的最简单方法。此方法可以接收可选的函数实现,该实现将透明地执行。这意味着运行模拟就像调用原始函数实现一样。在内部,jest.fn将跟踪所有调用并执行实现函数本身。
例如,如果我们想测试greetWorld如何使用greeting函数,我们可以传递一个模拟函数:
function greetWorld(greettingFn) {
return greetingFn('world');
}
test('greetWorld calls the greeting function properly', () => {
const greetImplementation = name => `Hey, ${name}!`;
const mockFn = jest.fn(greetImplementation);
const value = greetWorld(mockFn);
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('world');
expect(value).toBe('Hey, world!');
});
在这个测试中,我们将一个模拟函数传递给greetWorld函数。这个模拟函数有一个内部调用的实现。将模拟函数传递给greetWorld允许我们监视它如何使用该函数。我们期望该函数被调用一次,并将“world”字符串作为第一个参数。
它是如何工作的?
jest.fn方法本身是一个高阶函数。它是一种创建新的、未使用的模拟函数的工厂方法。此外,正如我们之前讨论的那样,JavaScript 中的函数是一等公民。每个模拟函数都有一些特殊属性。mock 属性是基础。此属性是一个对象,它具有有关如何调用该函数的所有模拟状态信息。此对象包含三个数组属性:
- 呼叫
- 实例
- 结果
在calls属性中,它将存储每次调用时使用的参数。instances属性将包含每次调用时使用的this值。results数组将存储函数每次调用退出的方式和退出时的值。
函数可以通过三种方式完成:
该函数明确返回一个值。
函数运行至完成,没有返回语句(相当于返回undefined)。
该函数抛出一个错误。
在results属性中,Jest 将函数的每个结果存储为具有两个属性的对象:type和value。 Type 可以是'return'或'throw'。value属性将包含返回值或抛出的错误。 如果我们从模拟实现本身内部测试结果,则类型将为'incomplete',因为该函数当前正在运行。
Jest 提供了一组自定义匹配器来检查有关函数调用方式的预期:
- 期望(fn).toBeCalled()
- 期望(fn).toBeCalledTimes(n)
- 期望(fn).toBeCalledWith(arg1,arg2,...)
- 期望(fn).lastCalledWith(arg1,arg2,...)
它们只是直接检查模拟属性的语法糖。
实现我们的模拟函数
没有比亲自实现更好的方法来理解某件事了。让我们从一个简单的模拟函数开始,只跟踪每次调用时使用的参数:
// 1. The mock function factory
function fn(impl) {
// 2. The mock function
const mockFn = function(...args) {
// 4. Store the arguments used
mockFn.mock.calls.push(args);
return impl(...args); // call the implementation
};
// 3. Mock state
mockFn.mock = {
calls: []
};
return mockFn;
}
第一个版本非常简单明了,让我们分解一下:
我们声明fn,这将是我们的模拟函数工厂,就像jest.fn一样。它接受一个实现函数作为参数。
在fn内部,我们定义mockFn,这是我们将返回的函数。
我们在函数对象中分配一个模拟属性。请记住,函数只是特殊的可调用对象,我们可以为它们分配属性。
在调用实现之前,我们记录函数调用中使用的参数。
请注意,在我们的mockFn中,我们使用ES6 剩余参数语法以数组的形式接收参数。
mockFn的mock属性,在第一个实现中,它只有一个属性,即调用。
如果我们想要实现另外两个功能,即跟踪每次调用的this值和函数的结果,我们需要改变几件事:
- 我们需要将实例和结果数组添加到我们的模拟状态对象中:
// 3. Mock state
mockFn.mock = {
calls: [],
instances: [],
results: [],
};
- 我们需要改变调用模拟实现的方式,以传递正确的值:
//...
const mockFn = function(...args) {
// 4. Store the arguments used
mockFn.mock.calls.push(args);
mockFn.mock.instances.push(this);
return impl.apply(this, args); // call impl, passing the right this
};
//...
Function.prototype.apply方法允许我们设置this值并应用参数数组。
- 我们需要将实现函数调用包装在try-catch语句中,以了解它是否抛出:
//...
const mockFn = function(...args) {
// 4. Store the arguments used
mockFn.mock.calls.push(args);
mockFn.mock.instances.push(this);
try {
const value = impl.apply(this, args); // call impl, passing the right this
mockFn.mock.results.push({ type: 'return', value });
return value;
} catch (value) {
mockFn.mock.results.push({ type: 'throw', value });
throw value; // re-throw error
}
};
<span cl
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~