I'm looking to test the call my function makes to another function, particularly an argument that is an object.
The issue is that sinon.js seems to store a reference to the object parameter given in its arguments array, which is naturally expected. However, this creates a problem when the parameter is later modified by ref, which changes the apparent value of those parameters at the time the function was called.
What's the best way to unit test a situation like this?
Here is a contrived example:
/
var view = Backbone.View.extend({
initialize: function () {
_.bindAll(this);
},
dostuff: function () {
var coords = { x: 0, y: 0 };
for (var i = 0; i < 10; i++) {
this.otherstuff(coords);
coords.x += 10;
}
},
otherstuff: function (coord) {
// Stubbed
}
});
test("a test", function() {
// Arrange
var blah = new view();
sinon.stub(blah, 'otherstuff');
// Act
blah.dostuff();
// Assert
var expectedFirstCallCoords = { x: 0, y: 0 };
// All assertions on the value of x for a particular call would seem to be impossible.
// blah.otherstuff.firstCall.args[0].x --> 100
deepEqual(blah.otherstuff.firstCall.args[0], expectedFirstCallCoords, 'parameter changed by ref, untestable?');
});
I can think of various hacks around this 'issue'. Is there a cleaner approach that doesn't involve cloning the object or modifying my production code simply for the sake of getting sinon to work?
I'm looking to test the call my function makes to another function, particularly an argument that is an object.
The issue is that sinon.js seems to store a reference to the object parameter given in its arguments array, which is naturally expected. However, this creates a problem when the parameter is later modified by ref, which changes the apparent value of those parameters at the time the function was called.
What's the best way to unit test a situation like this?
Here is a contrived example:
http://jsfiddle/xtfQu/
var view = Backbone.View.extend({
initialize: function () {
_.bindAll(this);
},
dostuff: function () {
var coords = { x: 0, y: 0 };
for (var i = 0; i < 10; i++) {
this.otherstuff(coords);
coords.x += 10;
}
},
otherstuff: function (coord) {
// Stubbed
}
});
test("a test", function() {
// Arrange
var blah = new view();
sinon.stub(blah, 'otherstuff');
// Act
blah.dostuff();
// Assert
var expectedFirstCallCoords = { x: 0, y: 0 };
// All assertions on the value of x for a particular call would seem to be impossible.
// blah.otherstuff.firstCall.args[0].x --> 100
deepEqual(blah.otherstuff.firstCall.args[0], expectedFirstCallCoords, 'parameter changed by ref, untestable?');
});
I can think of various hacks around this 'issue'. Is there a cleaner approach that doesn't involve cloning the object or modifying my production code simply for the sake of getting sinon to work?
Share Improve this question edited Oct 15, 2012 at 19:48 Adam Terlson asked Oct 15, 2012 at 19:28 Adam TerlsonAdam Terlson 12.7k4 gold badges46 silver badges63 bronze badges 6- This looks like a flaw in Sinon. It probably should clone the arguments, at least if they are native objects. Have you asked in their mailing list? Christian might either have a work-around, or be able to patch it quickly. Or he might well be able to suggest an alternate technique. I can't see anything that isn't fairly intrusive. – Scott Sauyet Commented Oct 15, 2012 at 19:48
-
@ScottSauyet Thanks for the confirmation that I'm not crazy. :) The only issue with sinon cloning those objects is that tests with strictEquals will fail against them. If I asserted on instead:
strictEquals(blah.otherstuff.firstCall.args[0], expectedFirstCallCoords)
that would fail against a clone. In my contrived example, that's no issue but what ifexpectedFirstCallCoords
were injected into the function/class? That'd be very undesired. Best case, that I can see anyway, is that it'd need to store original object references and cloned objects both, and that would be confusing. – Adam Terlson Commented Oct 15, 2012 at 19:51 - Yes, I didn't consider strictEquals. (Do you use that in testing? I never do.) I don't really see a great solution for this. But as to confirming that you're not crazy, I'm going to leave that between you and your shrink! :-) Best of luck! – Scott Sauyet Commented Oct 15, 2012 at 20:15
-
@ScottSauyet I, personally, consider use of
equal
to be of bad practice. Consider when you're testing your values thatequal(true, 1);
passes. – Adam Terlson Commented Oct 15, 2012 at 20:22 - I mostly use JSTestDriver, and the assertion framework's assertEquals is a deep equals pare, but not a reference parison. I don't use assertSame, or not often, which is a reference parison. But I also have, e.g., assertTrue, assertNotNull, to catch more specific types. On the other hand, I often want any truthy value to pass my boolean tests, so we may have very different standards! :-) – Scott Sauyet Commented Oct 15, 2012 at 20:31
1 Answer
Reset to default 7Credit to Mantoni on the related Github discussion on this:
var param = { property: 'value' };
var engage = sinon.stub();
var engageWithValue = engage.withArgs(sinon.match({ property : 'value' }));
engage(param);
param.property = 'new value';
sinon.assert.calledOnce(engageWithValue); // passes