Jest: Incorrect call arguments for recursive mock function

Created on 17 Oct 2017  路  7Comments  路  Source: facebook/jest

Bug Report

Current behaviour

When creating a mock function that uses recursion and an object argument, it appears to save the arguments for each call by "reference", rather than creating copy of them on each call. This results in the incorrect call arguments being saved as shown below:

class Test {
    testFunc(data) {
        if (data.value === 1) {
            data.value = 2;
            this.testFunc(data);
        } else if (data.value === 2) {
            data.value = 3;
            this.testFunc(data);
        }
    }
};
const myTest = new Test();

const testMock = jest.spyOn(myTest, 'testFunc');
myTest.testFunc({ value : 1 });

console.log(testMock.mock.calls);
// Outputs:  [ [ { value: 3 } ], [ { value: 3 } ], [ { value: 3 } ] ]
// Expected: [ [ { value: 1 } ], [ { value: 2 } ], [ { value: 3 } ] ]

This is demonstrated in a repl.it in the form of a jest test: Link

Expected behaviour

It should be copying the arguments for each call.

System Info
Jest: 21.2.1 with no additional configuration
Yarn: 1.2.1
OS: macOS 10.13

Most helpful comment

I wonder if we could have something like jest.fn().cloneArgs() which does a deep clone on a case by case basis?

@rickhanlonii thoughts on that one?

All 7 comments

That is true, but unfortunately doing a deep clone here would slow Jest down significantly :(

I see, that makes sense now that I think about it.

In that case, what are your thoughts on changing this to feature request instead to be able to specify whether a deep clone of arguments should be done on a per function basis?

Adapted method invocation example to confirm same for recursive function invocation:

test('recursive function invocation', () => {
  const testMock = jest.fn((data) => {
    if (data.value === 1) {
      data.value = 2;
      testMock(data);
    } else if (data.value === 2) {
      data.value = 3;
      testMock(data);
    }
  });
  testMock({ value : 1 });
  expect(testMock.mock.calls).toEqual([[{value: 1}], [{value: 2}], [{value: 3}]]);
});
    Expected value to equal:
      [[{"value": 1}], [{"value": 2}], [{"value": 3}]]
    Received:
      [[{"value": 3}], [{"value": 3}], [{"value": 3}]]

@jordansne Hello, can you let us know if this quick and rough helper works in your tests?

import {merge} from 'lodash';

function mapDeeply(arg) {
  if (Array.isArray(arg)) {
    return arg.map(mapDeeply);
  }
  if (arg !== null && typeof arg === 'object') {
    return merge({}, arg); // recursively merges own and inherited enumerable string keyed properties
  }
  return arg;
}

// Given object instance and methodName as string,
// return mock function in which arguments are copied deeply on each call.
function spyOnDeeply(object, methodName) {
  const outerMock = jest.fn();
  const method = object[methodName];
  const innerMock = jest.spyOn(object, methodName);
  innerMock.mockImplementation(function() {
    outerMock.apply(null, Array.prototype.map.call(arguments, mapDeeply));
    return method.apply(object, arguments);
  });
  return outerMock;
}

// replace: const testMock = jest.spyOn(myTest, 'testFunc');
// with: const testMock = spyOnDeeply(myTest, 'testFunc');

The so-called monkey patch idiom makes helper function shorter and maybe clearer:

function deepSpyOn(object, methodName) {
  const fn = jest.fn();
  const method = object[methodName];
  object[methodName] = function () {
    fn.apply(null, Array.prototype.map.call(arguments, deepArgMapper));
    return method.apply(object, arguments);
  }
  return fn;
}

Thank you for suggested way of doing this @pedrottimark (And yes, I know I've been a little late getting back on this, but better late than never..)

I will go ahead and close this issue as it's no longer relevant.

I wonder if we could have something like jest.fn().cloneArgs() which does a deep clone on a case by case basis?

@rickhanlonii thoughts on that one?

Was this page helpful?
0 / 5 - 0 ratings