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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mock.calls[][] holds reference instead of actual value when function called twice with object #434

Closed
sbugert opened this issue Jul 16, 2015 · 15 comments

Comments

@sbugert
Copy link

sbugert commented Jul 16, 2015

Consider the following test:

jest.dontMock('../unmocked');

describe('foo', function(){
  var unmocked = require('../unmocked');
  var mocked = require('../mocked');

  it('bar', function(){
    unmocked.func();
    var firstCall = mocked.func.mock.calls[0][0];
    var secondCall = mocked.func.mock.calls[1][0];

    expect(firstCall).toEqual({ content: 'first' });
    expect(secondCall).toEqual({ content: 'second' });

  });

});

With some arbitrary function being mocked in the test:

module.exports.func = function() {};

And a function which will be tested:

var mocked = require('./mocked');

module.exports.func = function() {
  var obj = { content: 'first' };
  mocked.func(obj);
  obj.content = 'second';
  mocked.func(obj);
};

The tested function calls the mocked function with an object, mutates the object and calls the mocked function a second time.
mocked.func.mock.calls[0][0] and mocked.func.mock.calls[1][0] will now hold the same reference to obj and the above test fails.
I'm not sure if that's intended.
If it's not, shouldn't jest make a deep copy of the argument if it's an object?

I'm using facebook/jest#0.5.x.

@browniefed
Copy link
Contributor

This is because you are only doing the require once.
Move your requires into a beforeEach

@sbugert
Copy link
Author

sbugert commented Jul 17, 2015

That does not solve the problem because I want to test if the function has been called with an object and then with the same object mutated in the same test. So the beforeEach will run only once.

jest.dontMock('../unmocked');

describe('getSearchResults', function(){
  var unmocked;
  var mocked;

  beforeEach(function() {
    unmocked = require('../unmocked');
    mocked = require('../mocked');
  });

  it('mock.calls should deep copy objects', function(){
    unmocked.func();
    var firstCall = mocked.func.mock.calls[0][0];
    var secondCall = mocked.func.mock.calls[1][0];

    expect(firstCall).toEqual({ content: 'first' });
    expect(secondCall).toEqual({ content: 'second' });

  });

});

@browniefed
Copy link
Contributor

@sbugert apologies misunderstood, I believe you just stumbled upon issue #429 or something similar.

@cpojer
Copy link
Member

cpojer commented Mar 3, 2016

Cloning here would be too expensive for performance. I understand this is an issue but the side-effect in your function is probably not something you'd want in your application anyway, I'll assume? While I do not know what your application is doing, shallow copying the object in your function Object.assign({}, obj) is probably reasonable.

@karolis-sh
Copy link

karolis-sh commented Sep 26, 2016

I'm facing the same issue. I'm constructing a nested object, validating it midway in the process, and use some final formatting in the end (date -> string). I'm mocking the validator, and have to do deep copy the object being passed to it.

It would be nice that fn.mock.calls would deep copy the values itself instead of me doing it in my code.

@Towerful
Copy link

Towerful commented Oct 9, 2017

I've just stumbled onto this as being an issue, using v21.2.1
Does anyone have a fix or a work around?

I build an object with 'old data', call a function to act on it, change 1 property, and call a function to act on it.
It seems a little ridiculous that I need to build up separate objects (or Object.assign a new one) for the sake of my test suite.

I feel like a mocked function should deep copy the parameters passed in.

@snooze92
Copy link

snooze92 commented Mar 7, 2018

Stumbled upon the same thing here. Any chance this gets revisited / reopened? Right now our production code is doing unnecessary copies, just so the test framework works as expected.

@et
Copy link

et commented Apr 5, 2018

Cloning here would be too expensive for performance. I understand this is an issue but the side-effect in your function is probably not something you'd want in your application anyway, I'll assume? While I do not know what your application is doing, shallow copying the object in your function Object.assign({}, obj) is probably reasonable.

@cpojer - Why would cloning be too expensive? Can't there be some kind of reasonable setting that only does it if we are asking about multiple calls? Or can we just tell jest somehow via a setting: "in this situation, I want you to clone".

As to your second question about why someone would do this without copying the object. I use typeorm and I have my code running in a loop to update a user's status from somewhere else -- this is from a 3rd party so I keep asking the 3rd party system what user's status is and save the result in my own db for caching purposes.

The code more or less looks like this:

async updateUserStatus(user) {
  const maxTries = 5;
  let tryNum = 1;

  while (tryNum <= maxTries) {
    await sleep(sleepTimeout);
    const resp = await client.getStatus(user.id);
    user.status = resp.status;

    await this.userRespository.save(user);

    if (user.status === 'success') {
      return Promise.resolve();
    }
  }
  this.log.info('Failed to get user\'s status in a sucess state');
}

I ran into this and spent some time trying to debug the situation.

I haven't used sinon in a long time but I believe they got this behavior correct. There is definitely a balance between performance and doing what the user intended. Right now, checking multiple mock calls in jest seems confusing.

I totally get the perf issues but surely there's a reasonable middle ground.

@SimenB
Copy link
Member

SimenB commented Apr 14, 2018

I wonder if it makes sense to add the ability to opt in to cloning

@elialgranti
Copy link

elialgranti commented Aug 29, 2018

I want to add my vote for an option to clone between calls.
In my case I am calling the S3 client to retrieve a list of files. S3 returns a maximum of 1000 files so it is called in a loop with the same requests parameters except for the continuation token. There is currently no way to test the continuation token is correctly copied from the result of the previous call to the next.

Deep cloning may be expensive, but it is better to have slightly slower tests than to have needless copies in production or code that cannot be tested.
Thanks!

@SimenB
Copy link
Member

SimenB commented Aug 29, 2018

I like my own idea above, and would love to see a PR.

jest.fn().cloneArgumentsAndReturnValues() is a way too long name, but you get the idea 🙂
Code lives here: https://github.com/facebook/jest/blob/master/packages/jest-mock/src/index.js

@joshribakoff
Copy link

I understand this is an issue but the side-effect in your function is probably not something you'd want in your application anyway, I'll assume?

Lets say you agree with this statement, with the current implementation of jest there is no way to write a test that verifies you don't do this.

Can't there be some kind of reasonable setting that only does it if we are asking about multiple calls?

That does not help if you have a single call with the wrong value, then after that the value gets mutated to the correct value. The test will pass, even though the mock was called with the wrong value.

@yatki
Copy link

yatki commented Apr 23, 2019

We recently encountered the same problem. I think instead of cloning the object in my source code, I'd prefer to waste some extra memory in the test runner.

@SimenB So having some methods like startArgumentRecording, stopArgumentRecording to opt-in argument recording sounds good to me. (names are not that good but to give you an idea)

One can even call jest.startArgumentRecording() in test setup file if they want to record everything...

So, @cpojer would you consider to add this functionality. Or is there already a solution for that?

@BreakBB
Copy link

BreakBB commented Jul 6, 2020

I stumpled across this as well today and nearly lost my mind, before I found out it really is an issue of jest itself.

I do understand the argument about performance, even though it is a clear side-effect caused by jest and not a real solution to add unnecessary Object.assigns to production code for tests.

The idea of @SimenB sounds like a solution which could fulfill performance requirements where object copies are not required.

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests