简体   繁体   English

在函数内模拟函数并开玩笑地获取调用计数

[英]mocking a function inside a function and getting calls count in jest

Considering this module in script.js :考虑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 };

In the test file below, shouldn't the call count of randomizeRangeMock be 100 because it is being called inside randomArrMock which is called once?在下面的测试文件中, randomizeRangeMock的调用计数不应该是 100,因为它是在调用一次的randomArrMock内部调用的吗? Why do I get 0为什么我得到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
});

I appreciate that you are trying to just understand jest with this question and so I will answer as best I can.我很感激你试图理解这个问题的jest ,所以我会尽我所能回答。 I'd normally recommend that you don't mock functions unless you are making some kind of complex back end call to a server or database or whatever.我通常建议您不要模拟函数,除非您对服务器或数据库或其他任何东西进行某种复杂的后端调用。 The more real testing you can do with real code (and not mocks) is infinitely more beneficial.您可以使用真实代码(而不是模拟)进行的更真实的测试会更加有益。 That would be my tip for you going forward.这将是我对你前进的建议。

But for the purpose of this question - here's what's going on.但就这个问题而言 - 这就是正在发生的事情。

The reason you're not seeing your mocks work in the right way here is because you've essentially created two separate mocks:你没有看到你的模拟在这里以正确的方式工作的原因是因为你基本上创建了两个单独的模拟:

  • You've created a single mock for randomizeArr .您已经为randomizeArr创建了一个模拟。
  • You've created another single, separate mock for randomizeRange .您已经为randomizeRange创建了另一个单独的模拟。

When you invoke randomArrMock(100, 20, 1000) in your test, this will invoke your single mock for randomizeArr as you've designated but that mock has absolutely no concept of the other mock for randomizeRange ever existing.当您在测试中调用randomArrMock(100, 20, 1000)时,这将按照您的指定为randomizeArr调用您的单个模拟,但该模拟绝对没有任何存在的randomizeRange模拟的概念。 And so it's never invoked behind the scenes.所以它永远不会在幕后被调用。 So this is why you see a 0 for the amount of calls at the end.所以这就是为什么你在最后看到呼叫数量为0原因。

It's just a matter of changing your setup a little bit here to designate which mocks are invoked and when in order to see both work together as you're expecting.只需在此处稍微更改您的设置以指定调用哪些模拟以及何时调用,以便按照您的期望看到两者一起工作。

What I would actually choose here is not to mock your randomizeRange method at all.我在这里实际选择的根本不是模拟您的randomizeRange方法。 That method invoked Math.floor and that's going to be much easier to "spy on" (which is a hint for the method we're going to use here) in this case.在这种情况下,该方法调用了Math.floor ,这将更容易“监视”(这是我们将在此处使用的方法的提示)。 That Math.floor method is invoked once with each call to randomizeRange and so it will be an ideal choice.每次调用randomizeRange都会调用一次Math.floor方法,因此它将是一个理想的选择。 It should also be called 100 times, the same as your method.它也应该被调用100次,与您的方法相同。

The reason we're choosing to spyOn the method rather than to use an actual mock in the way you have done already is because we want to keep the original behaviour and so spying on how many times the method is called without overwriting the intended behaviour is ideal.我们选择spyOn方法而不是按照您已经完成的方式使用实际模拟的原因是因为我们希望保留原始行为,因此在不覆盖预期行为的情况下监视调用该方法的次数是理想的。

So keep your existing mock for randomizeArr as follows:因此,请保留您现有的randomizeArr模拟,如下所示:

const randomArrMock = jest.fn(randomArr);

Now set up the spy for Math.floor现在为Math.floor设置间谍

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

This "spy" will effectively spy on this method without overwriting its behaviour.这个“间谍”将有效地监视此方法而不会覆盖其行为。 You can overwrite the behaviour if you want to but we want to keep the behaviour intact while just counting how many times it is called.如果您愿意,您可以覆盖该行为,但我们希望保持该行为完整无缺,同时只计算它被调用的次数。

So now when you run your test suite:所以现在当你运行你的测试套件时:

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)
});

This will now pass as Math.floor will have been invoked each time your randomizeRange method was called.现在这将通过,因为每次调用randomizeRange方法时都会调用Math.floor

Finally don't forget to restore the original Math.floor functionality when your test is finished by using:最后不要忘记使用以下命令在测试完成后恢复原始Math.floor功能:

mathFloorSpy.mockRestore();

Happy testing!测试愉快!

This seems to be a common question, and the most "official" solution I can find was on the Jest GitHub issues page, at https://github.com/facebook/jest/issues/936#issuecomment-545080082 .这似乎是一个常见问题,我能找到的最“官方”解决方案是在 Jest GitHub 问题页面上, 网址https://github.com/facebook/jest/issues/936#issuecomment-545080082

Since you are using require instead of ES modules transpiled with babel, the solutions described in that linked issue don't seem to apply as well.由于您使用的是require而不是使用 babel 转译的 ES 模块,因此该链接问题中描述的解决方案似乎也不适用。

I would modify your script.js module to directly reference the exports used, so that the mocks refer to the correct function:我会修改您的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;
};

And then your test implementation would reference the mocked functions:然后您的测试实现将引用模拟函数:

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);
});

Using spyOn and toHaveBeenCalledTimes improves readability here.使用spyOntoHaveBeenCalledTimes可以提高这里的可读性。


Now, giving me own piece of advice: Write testable code.现在,给我自己的建议:编写可测试的代码。 If your code is testable, well, you can easily write tests for it, and testable code is generally more modular and flexible;如果您的代码是可测试的,那么您可以轻松地为其编写测试,并且可测试的代码通常更加模块化和灵活; but don't focus on testing implementation details.但不要专注于测试实现细节。

In your example, if you want to expose a randomArr method, then don't also expose randomizeRange ;在您的示例中,如果您想公开一个randomArr方法,则不要同时公开randomizeRange your randomArr method can call randomizeRange , but that is an implementation detail you shouldn't worry about.您的randomArr方法可以调用randomizeRange ,但这是您不应该担心的实现细节。

If you want to make your randomArr much easier to test, consider making the range method (or the range) a parameter.如果您想让您的randomArr更易于测试,请考虑将范围方法(或范围)设为参数。 You can then test it without testing some implementation of how it generates random ranges.然后您可以测试它,而无需测试它如何生成随机范围的一些实现。 Something like this:像这样的东西:

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

You can extend this pattern to randomizeRange as well.您也可以将此模式扩展到randomizeRange For testing that method, either pass in the floor and random functions as parameters, or mock them (through the Math object) in your tests.为了测试该方法,要么将floorrandom函数作为参数传递,要么在测试中模拟它们(通过 Math 对象)。 Working with Math.random in tests is difficult since it will produce different values on each run, so it's important mock that.在测试中使用Math.random很困难,因为它会在每次运行时产生不同的值,所以重要的是模拟。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM