Search code examples
javascriptnode.jscallbackbluebirdsinon-chai

Test callback invocation at the end of promise chain


I am dealing with a code mixing node-style callbacks and Bluebird promises, and I need to write some unit tests for it.

In particular, cache.js exposes the init() function, which works with promises. It is then called by the doSomething() function in another file (e.g. index.js) which in turn accepts a callback that has to be invoked at the end of init().

Pseudocode is as follows:

// [ cache.js ]
function init() {
  return performInitialisation()
    .then((result) => return result);
}


// [ index.js ]
var cache = require('./cache');

function doSomething(callback) {
  console.log('Enter');

  cache.init()
    .then(() => {
      console.log('Invoking callback');
      callback(null);
    })
    .catch((err) => {
      console.log('Invoking callback with error');
      callback(err);
    });

  console.log('Exit');
}

A possible unit test could be (showing only relevant code):

// [ index.test.js ]
...
var mockCache = sinon.mock(cache);
...
it('calls the callback on success', function(done) {
  mockCache.expects('init')
    .resolves({});

  var callback = sinon.spy();

  doSomething(callback);
  expect(callback).to.have.been.calledOnce;
  done();
});

This test passes, however changing the expectation to not.have.been.calledOnce also passes, which is wrong.

Also, console logs are out of sequence:

Enter
Exit
Invoking callback

I have looked at several possibilities, none of which worked:

  • Using chai-as-promised, e.g. expect(callback).to.eventually.have.been.calledOnce;

  • Refactoring doSomething() to be simply:

    function doSomething(callback) { cache.init() .asCallback(callback); }

Can anyone help me understand what I am doing wrong and how I can fix it please?


Solution

  • console logs are out of sequence

    The logs are in the correct order because your Promise will be async meaning, at the very least, the internal console logs calls in then & catch will run on the next tick.

    As to why the test is failing is the result of a couple of issues, first one is you don't appear to have sinon-chai configured correctly, or at best your calledOnce assertion isn't kicking in. Just to confirm, the top of your test file should something like:

    const chai = require("chai");
    const sinonChai = require("sinon-chai");
    
    chai.use(sinonChai);
    

    If you have that and it's still not working correctly then might be worth opening an issue on the sinon-chai lib, however, a simple workaround is to switch to sinon assertions e.g.

    sinon.assert.calledOnce(callback)
    

    Secondly, when you do eventually fix this, you'll probably find that the test will now fail...everytime. Reason being you've got the same problem in your test that you have with your logging - your asserting before the internal promise has had a chance to resolve. Simplest way of fixing this is actually using your done handler from Mocha as your assertion

    mockCache.expects('init').resolves({});
    doSomething(() => done());
    

    In other words, if done get's called then you know the callback has been called :)