简体   繁体   中英

mocking a function inside a function and getting calls count in jest

Considering this module in 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? Why do I get 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. 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 .
  • You've created another single, separate mock for 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. 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.

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. 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. That Math.floor method is invoked once with each call to randomizeRange and so it will be an ideal choice. It should also be called 100 times, the same as your method.

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.

So keep your existing mock for randomizeArr as follows:

const randomArrMock = jest.fn(randomArr);

Now set up the spy for 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.

Finally don't forget to restore the original Math.floor functionality when your test is finished by using:

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 .

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.

I would modify your script.js module to directly reference the exports used, so that the mocks refer to the correct function:

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.


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 ; your randomArr method can call randomizeRange , but that is an implementation detail you shouldn't worry about.

If you want to make your randomArr much easier to test, consider making the range method (or the range) a parameter. 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. For testing that method, either pass in the floor and random functions as parameters, or mock them (through the Math object) in your tests. Working with Math.random in tests is difficult since it will produce different values on each run, so it's important mock that.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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