Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jest mocks calls are not recording object parameters as copies, but references, which results in bad assertions, when the object mutates later. #10373

Open
marcelb opened this issue Aug 6, 2020 · 2 comments

Comments

@marcelb
Copy link

marcelb commented Aug 6, 2020

#10373 馃悰 Bug Report

When asserting multiple calls to the same mock function using objects as parameters to the mock function, Jest will not record the values of the object as they were at the moment of the call, but only the reference to the object.

When tested code mutates the object after the call, it will also mutate the recorded calls within Jest, resulting in failing tests, that should be green.

To Reproduce

Using Jest 26.2.2, consider this test:

let doingSomething = jest.fn();

function myFunction() {
    let myParam = { myField: 'testValue' };
    doingSomething(myParam);
    myParam.myField = 'differentValue';
    doingSomething(myParam);
}

describe('something', () => {
    test('should work',  () => {
        myFunction();
        expect(doingSomething).toBeCalledTimes(2);

        // this assert will fail, because myField == 'differentValue', even though it shouldn't:
        expect(doingSomething).toHaveBeenNthCalledWith(1, { myField: 'testValue' }); 

        // this is okay:
        expect(doingSomething).toHaveBeenNthCalledWith(2, { myField: 'differentValue' });
    });
});

Expected behavior

Both expects should be green, since the object parameter was correct at the time of the first call.

@kfiku
Copy link

kfiku commented Jan 4, 2023

Hello @marcelb

I don't think it's a bug. This is how JavaScript programming language works with object and other mutable objects.

Here you can find other approach to the same problem: https://codesandbox.io/s/practical-water-26cf0x?file=/main.test.js

let doingSomething = jest.fn();

function myFunction() {
  let myParam = { myField: "testValue" };
  doingSomething(myParam);
  myParam.myField = "differentValue";
  doingSomething(myParam);
}

describe("something", () => {
  beforeEach(() => {
    doingSomething.mockReset();
  });

  test("should realy work", () => {
    let calledTimes = 0;
    doingSomething.mockImplementation((myParam) => {
      calledTimes++;
      switch (calledTimes) {
        case 1:
          expect(myParam).toEqual({ myField: "testValue" });
          break;
        case 2:
          expect(myParam).toEqual({ myField: "differentValue" });
          break;

        default:
          break;
      }
    });

    myFunction();
    expect(doingSomething).toBeCalledTimes(2);
  });

  test("should NOT work", () => {
    myFunction();
    expect(doingSomething).toBeCalledTimes(2);

    // this assert will fail, because myField == 'differentValue', even though it shouldn't:
    expect(doingSomething).toHaveBeenNthCalledWith(1, { myField: "testValue" });

    // this is okay:
    expect(doingSomething).toHaveBeenNthCalledWith(2, {
      myField: "differentValue"
    });
  });
});

To better understand this problem you can imagine what is happening in similar pure js example:

// VERY simple ala jest.fn
function makeMockFn () {
  const callHistory = []

  const mockedFn = (...args) => {
    callHistory.push(args)
  }

  // helper to get function call history
  mockedFn.getCallHistory = () => callHistory

  return mockedFn
}

// creating ala jest.fn()
const fn = makeMockFn()

// create params to pass to fn
const myParams = { a: 1, b: 2 }

// running fn
fn(myParams)
fn('string')
fn(123456)

// check what we have in call history
console.log(fn.getCallHistory())

// lets mutate myParams
myParams.a = 'CHANGED a key'

// try to run fn
fn(myParams)

// check what happen with first and last position of history array
console.log(fn.getCallHistory())

@krisb
Copy link

krisb commented Jul 5, 2023

There are earlier reference to the same underlying issue in #434 and #429.

A simple approach would be to support cloning the parameters for specific calls where this is a known issue due to the implementation (called out here: #434 (comment))

Some other comments have said that its not good practice to mutate the objects in the code, however, I think a testing framework shouldn't be pushing a particular coding practice.

An overall better approach IMHO is that of sinon where the mock is set to expect certain values, or support for conditional mocking could be used also.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants