Using sinon.spy to track callCount always returns 0 on nested functions

Hello, I’m having trouble with sinon.spy, I’m using meteortesting:mocha/chai/sinon when I try to write a test to check if a nested function was invoked it always returns 0

const a = function () {
b();
}
const b = function () {
console.log('on b function');
}
// export both a and b so I can import { a } from 'a.js'; import { b } from 'b.js';

it('Should have called b', function () {
        const spyB = sinon.spy(b);
        a();
        console.log(spyB.callCount); // returns 0
        sinon.assert.calledOnce(spyB);
    });
// test fails as it nevers records the call to b() even though I see the console log that it was invoked

Is there something I missed? hope the example makes sense.

Thanks!

In your example a is still using b directly. The spy can’t replace the variable reference that a already holds in a separate module. This is partially because imported values are immutable, and partially because const.

The easiest way around this is to have every function belong to a namespace / object / static class because properties of an object can be changed:

const NS = {}
NS.a = function () {
NS.b();
}
NS.b = function () {
console.log('on b function');
}
// export NS, which can be destructured into a and b

import NS from './ns.js';
it('Should have called b', function () {
        const spyB = sinon.spy(NS, 'b');
        NS.a();
        console.log(spyB.callCount); // returns 1
        sinon.assert.calledOnce(spyB);
        spyB.restore() // remove spy from method
    });

The more complicated way is to use dependency injection

1 Like

Thanks for your detailed explanation @coagmano, makes a lot of sense that due immutability I can’t replace those variable references with sinon.spy, Will try out your approach!

I found a clean way to implement sinon spies/stubs/mocks and overcome inmutability by using babel-plugin-rewire-exports https://www.npmjs.com/package/babel-plugin-rewire-exports

const a = function () {
b();
}
const b = function () {
console.log('on b function');
}
// export both a and b so I can import { a } from 'a.js'; import { b } from 'b.js';

// on a.test.js
import { a } from './a.js';
import { rewire$b, restore } from './b.js';

it('Should have called b', function () {
        let spyB;
        rewire$b(spyB = sinon.spy());
        a();
        console.log(spyB.callCount); // returns 1
        sinon.assert.calledOnce(spyB); // Passes!!
        restore();
    });

So for this to work you need to add

{
  "plugins": [
    ["rewire-exports", {
      "unsafeConst": true
    }]
  ]
}

to your babel configuration (.babelrc, or packages.json either should do the trick)

Hope this helps someone that runs into the same problem!

1 Like