在 promise 链的末尾测试回调调用

Test callback invocation at the end of promise chain

我正在处理混合节点式回调和 Bluebird 承诺的代码,我需要为其编写一些单元测试。

特别是,cache.js 公开了 init() 函数,它与 promises 一起工作。然后它被另一个文件(例如 index.js)中的 doSomething() 函数调用,该文件又接受必须在 init().

结束时调用的回调

伪代码如下:

// [ 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');
}

可能的单元测试可能是(仅显示相关代码):

// [ 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();
});

这个测试通过了,但是将期望更改为 not.have.been.calledOnce 也通过了,这是错误的。

此外,控制台日志顺序不正确:

Enter
Exit
Invoking callback

我研究了几种可能性,none 可行:

任何人都可以帮助我了解我做错了什么以及如何解决它吗?

console logs are out of sequence

日志的顺序正确,因为您的 Promise 将是异步的,这意味着,至少,内部控制台日志调用 thencatch 将 运行 在下一个报价单上。

至于为什么测试失败是几个问题的结果,第一个是你似乎没有正确配置 sinon-chai,或者充其量你的 calledOnce 断言是还没开始。只是为了确认,测试文件的顶部应该是这样的:

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

chai.use(sinonChai);

如果你有它,但它仍然不能正常工作,那么可能值得在 sinon-chai 库上打开一个问题,但是,一个简单的解决方法是切换到 sinon assertions,例如

sinon.assert.calledOnce(callback)

其次,当您最终解决此问题时,您可能会发现测试现在会失败……每次都失败。原因是您在测试中遇到了与日志记录相同的问题——您在内部承诺有机会解决之前的断言。解决此问题的最简单方法实际上是使用 Mocha 中的 done 处理程序作为断言

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

换句话说,如果 done 被调用,那么您知道回调已被调用:)

根据 James' 的评论,我像这样重新访问了我的测试:

it('calls the callback on success', function(done) {
  mockCache.expects('init')
    .resolves({});

  doSomething(done);
});

it('calls the callback on error', function(done) {
  mockCache.expects('init')
    .rejects('Error');

  doSomething((err) => {
    if (err === 'Error') {
      done();
    } else {
      done(err);
    }
  });
});