Search code examples
javascriptunit-testingecmascript-6spysinon-chai

How to spy on a sub-function of a named export in ES6


I want to spy on a sub-function that is exported as a named export, but, it seems like we cannot spy on it.

Let's say I have two functions called add and multiply in operations.js and export them as named exports:

const add = (a, b) => {
  return a + b
}

const multiply = (a, b) => {
  let result = 0

  for (let i = 0; i < b; i++) {
    result = add(a, result)
  }
  return result
}

export { add, multiply }

And the test file uses sinon-chai to try to spy on the add function:

import chai, { expect } from 'chai'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import * as operations from './operations'

chai.use(sinonChai)

describe('multiply', () => {
  it('should call add correctly', () => {
      sinon.spy(operations, 'add')

      operations.multiply(10, 5)
      expect(operations.add).to.have.been.callCount(5)

      operations.add.restore()
  })
})

The result is

AssertionError: expected add to have been called exactly 5 times, but it was called 0 times

But, if I calls operations.add directly like the following, it passes the test:

describe('multiply', () => {
  it('should call add correctly', () => {
      sinon.spy(operations, 'add')

      operations.add(0, 5)
      operations.add(0, 5)
      operations.add(0, 5)
      operations.add(0, 5)
      operations.add(0, 5)
      expect(operations.add).to.have.been.callCount(5)

      operations.add.restore()
  })
})

It seems like sinon-spy creates a new reference for operations.add but the multiply function still uses the old reference that was already bound.

What is the correct way to spy on the add function of this multiply function if these functions are named exports?

Generally, how to spy on a sub-function of a tested parent function which both are named exports?

[UPDATE]

multiply function is just an example. My main point is to test whether a parent function calls a sub-function or not. But, I don't want that test to rely on the implementation of the sub-function. So, I just want to spy that the sub-function is called or not. You can imagine like the multiply function is a registerMember function and add function is a sendEmail function. (Both functions are named exports.)


Solution

  • I have a workaround for my own question.

    Current, the multiply() function is tightly coupling with add() function. This makes it hard to test, especially, when the exported functions get new references.

    So, to spy the sub-function call, we could pass the sub-function into the parent function instead. Yes, it's dependency injection.

    So, in operations.js, we will inject addFn into multiply() and use it as follows:

    const add = (a, b) => {
      return a + b
    }
    
    const multiply = (a, b, addFn) => {
      let result = 0
    
      for (let i = 0; i < b; i++) {
        result = addFn(a, result)
      }
      return result
    }
    
    export { add, multiply }
    

    Then, in the test, we can spy on add() function like this:

    describe('multiply', () => {
      it('should call add correctly', () => {
          sinon.spy(operations, 'add')
    
          operations.multiply(10, 5, operations.add)
          expect(operations.add).to.have.been.callCount(5)
    
          operations.add.restore()
      })
    })
    

    Now, it works for the purpose of testing whether the sub-function is called correctly or not.

    (Note: the drawback is we need to change the multiply() function)