Search code examples
javascriptchaisinonnock

Wrong count execution times of Sinon Spy


I have a major function that call 3 other functions. I want to test if those 3 function were called.

myController.ts

import axios from 'axios';

export async function functionOne() {
  console.log('Hit functionOne');
  const result = await axios.get('https://www.urlOne.com/first');
  return result;
}

export async function functionTwo() {
  const result = await axios.get('https://www.urlTwo.com/second');
  return result;
}

export async function functionThree() {
  const result = await axios.get('https://www.urlThree.com/third');
  return result;
}

export async function mainFunction() {
  const resultOne = await functionOne();
  // some logic that also calls functionTwo & functionThree
}



test.spec.ts

import * as myCtrl from '../controllers';
import { expect } from 'chai';
import * as sinon from 'sinon';
import * as sinonChai from 'sinon-chai';
import * as chaiAsPromised from 'chai-as-promised';

chai.use(sinonChai);
chai.use(chaiAsPromised);
chai.should();

describe('success', () => {
  const sandbox = sinon.createSandbox();

  // Create Spies
  beforeEach(() => {
    sandbox.spy(myCtrl, 'functionOne');
    sandbox.spy(myCtrl, 'functionTwo');
    sandbox.spy(myCtrl, 'functionThree');
  });

  afterEach(() => {
    sandbox.restore();
  })


  it('test something', async () => {
    nock('https://www.urlOne.com')
      .get('first')
      .reply(200)

    nock('https://www.urlTwo.com')
      .get('second')
      .reply(200)

    nock('https://www.urlThree.com')
      .get('third')
      .reply(200)

    await myCtrl.mainFunction();

    expect(myCtrl.functionOne).to.be.calledOnce();
    expect(myCtrl.functionTwo).to.be.calledOnce();
    expect(myCtrl.functionThree).to.be.calledOnce();
  });
});

It looks weird, but according to the nock documentation, using multiple nocks is the right way.

Also tried using axios-mock-adapter instead of Nock, but the same error occurs.

The problem is, it always returns:

AssertionError: expected functionOne to have been called exactly once, but it was called 0 times

I can see the console.log message on console, but the count still 0. Am I missing something here?


Solution

  • It's an import scope issue.

    Stubbing or spying a module depends on export style. The mentioned case imports separate exports and assemble them into one.

    import * as myCtrl from '../controllers';
    

    The problem is that functionOne,two,three in mainFunction() are different from the ones that you've imported and spied. sinon can't expose and modify the function's inner scope. It can only modify what it can access.

    // for convenience, let's say
    // functionOne, functionTwo, functionThree = f1,f2,f3
    
    // controller.js
    export function f1() {}
    export function f2() {}
    export function f3() {}
    export function mainFunction() { f1(); f2(); f3() }
    
    // spying(stubbing) in test
    import { f1, f2, f3, mainFunction } from './controller';
    
    // it does not wrap or modify mainFunction.f1()
    // because,
    // f1 !== mainFunction.f1
    sinon.spy(f1)
    

    Solution

    1. Just disconnect inner functions and separate the logic layer as a pure function. I personally like this approach because it's the simplest and less likely to cause dependency issues when testing.
    // get the functions as arguments, so that you can test the pure logic
    // controller.js
    export function f1() {}
    export function f2() {}
    export function f3() {}
    export function mainFunction(f1,f2,f3) { f1(); f2(); f3(); }
    
    // test
    import { mainFunction } from 'controller';
    describe('', () => {
      let f1;
      // ...
      before(() => {
        // ...
        f1 = sinon.spy(require('controller').f1);
      })
      it('test', () => {
        mainFunction(f1, f2, f3);
        // check the spy from here
      })
    })
    
    1. separate functions into different modules then stub a dependency of the module

    2. make the module as a whole, and modify the specific property.

    // controller
    // it does not have to be a class. just make the inner functions to share the same scope.
    class Controller {
      constructor() {}
      f1() {}
      f2() {}
      f3() {}
      mainFunction() {
        this.f1(); // ...implement logic here
      }
    }
    
    // test
    import Controller from 'controller';
    
    describe('', () => {
      let controller;
    
      before(() => {
        controller = new Controller();
        sinon.spy(controller.f1);
      })
    
      it('test', () => {
        controller.mainFunction()
        // check the spy from here
      })
    
    })