Jasmine Gotcha: spyOn(…).and.callThrough() makes only a shallow copy of arguments

Alexey Feigin
2 min readSep 1, 2019

I was recently writing some frontend JavaScript tests using the Jasmine framework, and came across this little issue I’ll describe here.

Suppose we want to test if a method is called, but also want it to execute it.

(Excuse the ES5-style method definition…)

We would like to test that innerMethodReturning0 is called with the correct argument, but also for some reason want it to execute. In this case, test that innerMethodReturning0 is being called with the correct config.

(In reality we should test innerMethodReturning0 separately instead of calling through… This is contrived in the interests of keeping it simple.)

This may be fine, but let’s consider what happens if innerMethodReturning0 mutates its argument.

This works.

Now let’s consider the case where innerMethodReturning0 mutates a deep property of the argument. For example, it could set its own default setting of config.subConfig.option2: true on the config object.

In this case the test will fail with:

Expected obj.innerMethodReturning0 to have been called with
{ subConfig: { option: true } }
but was called with
{ subConfig: { option: true, option2: true } }.

This is because Jasmine only makes a shallow copy of the actual arguments at the entry to the spy, to use for comparison later. This means that if innerMethodReturning0 mutates a deep property on the argument, the actual argument object tree will also be mutated.

The following is one partial workaround, in which we maintain our own deep clone of the argument.

In general, deep cloning in JavaScript is suspect because error objects, functions, DOM nodes, and WeakMaps cannot be cloned (not to mention circular references in objects).

I have not tested this in Mocha or other testing frameworks, but I suspect that due to the CPU cost and limitations of deep cloning they would suffer from similar problems with a setup like this. (Please write in the comments if you know.)

It is probably best to avoid the spyOn(…).and.callThrough() pattern when possible. Definitely avoid when the arguments may be mutated.

(Thanks to Ben Woodcock and Yaakov Smith for their feedback on this piece.)

--

--