在函数内部嘲笑一个函数并获得调用计数

时间:2019-11-23 17:06:25

标签: javascript unit-testing mocking jestjs

script.js中考虑此模块:

const randomizeRange = (min, max) => {
  return Math.floor(Math.random() * (max - min) + min);
};

const randomArr = (n, min, max) => {
  const arr = [];
  for (let i = 0; i < n; i++) {
    arr.push(randomizeRange(min, max));
  }
  return arr;
};


module.exports = { randomArr, randomizeRange };

在下面的测试文件中,randomizeRangeMock的调用计数不应该为100,因为它在一次调用过的randomArrMock内部被调用了吗?为什么我得到0

const randomArr = require("./script").randomArr;
const randomizeRange = require("./script").randomizeRange;

const randomArrMock = jest.fn(randomArr);
const randomizeRangeMock = jest.fn(randomizeRange);

test("tests for randomArr", () => {
  expect(randomArrMock(100, 20, 1000)).toHaveLength(100);
  expect(randomArrMock.mock.calls.length).toBe(1)
  expect(randomizeRangeMock.mock.calls.length).toBe(100) //This fails because its 0
});

2 个答案:

答案 0 :(得分:1)

我感谢您尝试通过这个问题来理解ThreadPool,因此,我将尽力回答。我通常建议您不要模拟函数,除非您要对服务器或数据库进行某种复杂的后端调用。您可以使用实际代码(而不是模拟)执行的更真实的测试将无限地受益。那将是我前进的秘诀。

但是出于这个问题的目的-这是正在发生的事情。

之所以无法在此处看到正确的模拟效果,是因为您实际上已经创建了两个单独模拟:

  • 您已经为jest创建了一个模拟。
  • 您已经为randomizeArr创建了另一个单独的模拟。

在测试中调用randomizeRange时,这将为您指定的randomArrMock(100, 20, 1000)调用单个模拟,但是该模拟绝对没有randomizeArr的其他模拟的概念现有。因此,它从未在后台调用。因此,这就是为什么您在通话结束时看到randomizeRange的原因。

只需在此处稍微更改一下设置即可指定要调用哪些模拟程序,以及何时才能使两者按预期工作在一起。

我实际上在这里选择的是来完全模拟您的0方法。在这种情况下,该方法调用了randomizeRange,这将更容易“监视”(这是我们将在此处使用的方法的提示)。每次调用Math.floor都会调用一次Math.floor方法,因此这是一个理想的选择。它也应该被调用randomizeRange次,与您的方法相同。

我们选择使用100方法而不是按照您已经做过的方式使用实际模拟的原因是,因为我们想保留原始行为,因此监视该方法有多少次调用而不覆盖预期的行为是理想的。

因此,请按以下步骤保留spyOn的现有模拟:

randomizeArr

现在为const randomArrMock = jest.fn(randomArr); 设置间谍

Math.floor

此“间谍”将有效监视此方法,而不会覆盖其行为。如果需要,您可以覆盖该行为,但是我们希望在保持行为不变的同时,只计算该行为被调用的次数。

现在,当您运行测试套件时:

const mathFloorSpy = jest.spyOn(Math.prototype, `floor`);

现在将通过,因为每次调用test("tests for randomArr", () => { expect(randomArrMock(100, 20, 1000)).toHaveLength(100); expect(randomArrMock.mock.calls.length).toBe(1) expect(mathFloorSpy.mock.calls.length).toBe(100) }); 方法时都会调用Math.floor

最后,不要忘记使用以下方法来恢复测试的原始randomizeRange功能:

Math.floor

测试愉快!

答案 1 :(得分:1)

这似乎是一个常见问题,我能找到的最“官方”解决方案是在Jest GitHub的问题页面,https://github.com/facebook/jest/issues/936#issuecomment-545080082

由于您使用的是require而不是使用babel编译的ES模块,因此该链接问题中描述的解决方案似乎也不适用。

我将修改您的script.js模块以直接引用使用的导出,以便模拟引用正确的函数:

exports.randomizeRange = (min, max) => {
    return Math.floor(Math.random() * (max - min) + min);
};

exports.randomArr = (n, min, max) => {
    const arr = [];
    for (let i = 0; i < n; i++) {
        arr.push(exports.randomizeRange(min, max));
    }
    return arr;
};

然后您的测试实现将引用模拟的函数:

const myScript = require('../script');

jest.spyOn(myScript, 'randomArr');
jest.spyOn(myScript, 'randomizeRange');

test("tests for randomArr", () => {
    expect(myScript.randomArr(100, 20, 1000)).toHaveLength(100);
    expect(myScript.randomArr).toHaveBeenCalledTimes(1);
    expect(myScript.randomizeRange).toHaveBeenCalledTimes(100);
});

在这里使用spyOntoHaveBeenCalledTimes可以提高可读性。


现在,给我自己一条建议:编写可测试的代码。如果您的代码是可测试的,那么您可以轻松地为其编写测试,并且可测试的代码通常更具模块化和灵活性。但不要专注于测试实施细节。

在您的示例中,如果您想公开randomArr方法,那么也不要公开randomizeRange;您的randomArr方法可以调用randomizeRange,但这是您不需担心的实现细节。

如果您想更轻松地测试randomArr,请考虑将range方法(或range)设为参数。然后,您可以测试它,而无需测试如何生成随机范围的某些实现。像这样:

exports.randomArr = (n, min, max, randomRangeGenerator) => {
    const arr = [];
    for (let i = 0; i < n; i++) {
        arr.push(randomRangeGenerator(min, max));
    }
    return arr;
};

您也可以将此模式扩展到randomizeRange。为了测试该方法,可以将floorrandom函数作为参数传递,或者在测试中模拟它们(通过Math对象)。在测试中使用Math.random很困难,因为每次运行都会产生不同的值,因此进行模拟非常重要。