Search code examples
javascripttestinglodashsinonava

sinon.getCall(0).args[0] is returning the result of the function, not first arg


I am testing a small function using AVA and Sinon. Function looks essentially like this (edited for brevity):

mergeDefaults: function (opts) {
  **console.log('log 1 ->', opts);**
  opts = _.defaultsDeep(opts, defaultOptions);
  return opts;
}

I have written a test to ensure that the correct parameters are passed into _.defaultsDeep.

test.before(t => {
  sandbox = sinon.sandbox.create();
  defaultsDeepSpy = sandbox.spy(_, 'defaultsDeep');
  mergeDefaults(Object.assign({}, testOptions));
});

test('mergeDefaults runs _.defaultsDeep with the correct parameters', t => {
  **console.log('log 2 ->', defaultsDeepSpy.getCall(0).args[0]);**
  t.is(defaultsDeepSpy.getCall(0).args[0], testOptions);
  t.is(defaultsDeepSpy.getCall(0).args[1], defaultOptions);
});

test.after.always(t => {
  sandbox.restore();
});

The problem I am running into is that log 1 and log 2 are not the same. As I understand it, the

spy.getCall(n).args[m]

returns the mth argument passed to the spied on function from the nth call to that function. However, in this case log 2 is actually returning what results from the function, not the first argument.

The options objects look something like this:

testOptions = {
  key1: 11,
  key2: 22,
  key3: 33
}
defaultOptions = {
  key1: 1,
  key2: 2,
  key3: 3,
  key4: 4,
  key5: 5
}

But the console.logs look like this:

log 1 -> testOptions = {
  key1: 11,
  key2: 22,
  key3: 33
}
log 2 -> testOptions = {
  key1: 11,
  key2: 22,
  key3: 33,
  key4: 4,
  key5: 5
}

So the spy.getCall(0).args[0] a.k.a "log 2" is actually returning what the "opts" becomes after the _.defaultsDeep runs. What am I doing wrong?


Solution

  • The root issue you are having is that the reference to the parameter (in this case opts) is the same object that you are manipulating with your values (as all objects are passed by reference in JavaScript). In effect, you are changing the physical location that opts points to.

    As Sinon doesn't take a "snapshot" or "copy" of the objects that you are passing in (it just returns the same object references that you are passing in), you are ALSO affecting what Sinon is returning.

    To solve this issue the code should look something like

    mergeDefaults: function (opts) {
        return _.defaultsDeep(Object.assign({}, opts), defaultOptions);
    }
    

    The Object.assign will create a new object (and associated place in memory). As such, it will leave opts untouched so you can check what was passed in.