Unable to stub a function using Sinon

Viewed 2352

I have a Redux action, which itself dispatches two other actions. Each action is retrieved from an imported function. One from a local module, the other from an external library.

import { functionA } from './moduleA';
import { functionB } from 'libraryB';

export function myAction() {
  return (dispatch) => {
    dispatch(functionA());
    ...
    dispatch(functionB());
  }
}

In my tests I'm using a sinon sandbox to stub out the functions but only two of the tests pass. I'm expecting all 3 to pass.

import * as A from './moduleA';
import * as B from 'libraryB';

describe(__filename, async () => {
  it('Calls 2 other actions', () => {
    sandbox = sinon.sandbox.create();

    const dispatch = sandbox.stub();

    sandbox.stub(A, 'functionA');
    sandbox.stub(B, 'functionB');

    await myAction()(dispatch);

    // passes
    expect(dispatch.callCount).to.equal(2);

    //passes
    expect(A.functionA).to.have.been.called();

    // fails
    expect(B.functionB).to.have.been.called();     
  });
});

The last expect fails with the error:

TypeError: [Function: functionB] is not a spy or a call to a spy!

When I output the functions to the console I get this, which seems to be related to the way Babel imports an exported export (ES6 re-exported values are wrapped into Getter). The functions work live, just not in the test.

{ functionA: [Function: functionA] }
{ functionB: [Getter] }

I've tried using stub.get(getterFn) but that just gives me the error:

TypeError: Cannot redefine property: fetchTopicAnnotations

2 Answers

Have you tried naming your stubs? Your code reads a bit weird. You're not referring to your stubs at any point in your test.

import * as A from './moduleA';
import * as B from 'libraryB';

describe(__filename, async () => {
  it('Calls 2 other actions', () => {
    sandbox = sinon.sandbox.create();

    const dispatch = sandbox.stub();

    const functionAStub = sandbox.stub(A, 'functionA');
    const functionBStub = sandbox.stub(B, 'functionB');

    await myAction()(dispatch);

    // passes
    expect(dispatch.callCount).to.equal(2);

    //passes
    expect(functionAStub.called).toBe(true);

    // fails
    expect(functionBStub.called).toBe(true);     
  });
});

Its hard to say 100% but it appears that the module is importing and therefore has a direct reference to the function before your test is stubbing. Sinon will effectively replace the function on the exports object but the other module will have imported and gotten a reference to real function first, replacing it won't cause it to reload.

You could experiment with it and prove it by altering your module to be this:

import * as A from './moduleA';
import * as B from 'libraryB';

export function myAction() {
  return (dispatch) => {
    dispatch(A.functionA());
    ...
    dispatch(B.functionB());
  }
}

That will essentially look up the reference to functionA/functionB at the time of invocation which will allow sining to replace them with a stub.

If you find that gross and want to preserve the original form then I believe you need to use a second library called proxyquire, which will effectively let you stub the entire module being imported.

Your test functions would end up looking more like this:

import * as proxyquire from 'proxyquire'
// import * as A from './moduleA'
// import * as B from 'libraryB'

describe(__filename, async () => {
  const A = {}
  const B = {}
  const functionAStub = sandbox.stub(A, 'functionA')
  const functionBStub = sandbox.stub(B, 'functionB')
  const { myAction } = proxyquire('./myAction', {
    './moduleA': A,
    'libraryB': B
  })
  ...
})

The module under test will not be imported until you call proxyquire when it is require'ing the module it will use your stubbed modules instead of the real ones.

You can then reference those stubs from within your tests as expected.

Related