4

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.)

Supasate
  • 1,564
  • 1
  • 16
  • 22
  • 3
    Speaking from experience, tests like this end up being quite brittle. The return value of `multiply` is what is important, not how it is implemented. Better to focus on testing that. – loganfsmyth May 20 '16 at 03:59
  • 1
    `export { add, multiply }` is similar to `export { add : add, multiply : multiply }`. So the exported object contains references to the original methods, and `sinon.spy()` replaces the reference but not the original (which is what `multiply()` uses). – robertklep May 20 '16 at 07:44
  • @loganfsmyth there are some situations that a function doesn't return any value but it will call other function to do something. So, we'd like to test whether it call that other function or not. – Supasate May 20 '16 at 12:19
  • @robertklep I think so. So, I'd like to know whether is it possible to spy on it. (It's not necessary to be sinon.) – Supasate May 20 '16 at 12:32
  • @Supasate I don't think you can spy on it because `multiply()` isn't using the exported reference. – robertklep May 20 '16 at 13:43
  • @robertklep I think of decoupling `multiply()` and `add()` first. I try to answer my own question below. – Supasate May 20 '16 at 14:10

1 Answers1

-2

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)

Supasate
  • 1,564
  • 1
  • 16
  • 22
  • 1
    You change your implementation for testing purpose. It's bad practice. We write tests for code, not code for tests. – vp_arth May 20 '16 at 14:58
  • 3
    @vp_arth Changing implementation for testing pupose is ok. You should make your code testable. If you can't test your code this is legacy code now and this is bad. The problem here I think is OP is checking if add is called multiple times and this is not important normally. You should test the contract and not implementation details. – Marc-Andre May 20 '16 at 15:47
  • 1
    i think your primary issue is you are testing how many times `add` is called. as a result, you are always assuming that `multiply` will use `add`. This is tight coupling and a bad test IMO. Really, the only time you want to use spies or mocks is when you want to return certain results or when you are testing callbacks (which are part of the interface). Multiply calling add X number of times is implementation detail. – Dan May 20 '16 at 15:49
  • It's not exact `multiply` function now. Just pass `(a,b)=>a*b` in and it becomes `power` function :) – vp_arth May 20 '16 at 16:08
  • Actually, the main point is to test that the parent function calls the sub-function correctly and don't want to rely on the implementation of the sub-function to isolate the test. I just made `multiply` function to be an example (my bad with that). – Supasate May 20 '16 at 16:48