Mocha Chai Sinon测试无法访问的承诺/ async / event-emitter

时间:2017-04-18 12:56:47

标签: node.js testing promise sinon chai

我的设置使用chaisinonchai-sinonchai-as-promisedbabel和es6语法。

我有以下(简化)代码

// example.js
'use strict';
import EventEmitter from 'events';

class Dispatcher extends EventEmitter {
  send(x) {
    doSomethingAsync() // promise is NOT returned 
      .then(() => {
        this.emit('sent');
      })
      .catch((err) => {
        this.emit('error', err);
      });
  }
}

注意:不会返回doSomethingAsync的承诺。 (永远不会)

这是我的(简化的)测试文件

let dispatcher;
let onSent;
let onError;

beforeEach(() => {
  dispatcher = new Dispatcher();

  onSent = sinon.stub();
  onError= sinon.stub();
  dispatcher.on('sent', onSent);
  dispatcher.on('error', onError);
});

describe('send', () => {
  it('should emit "error" on sendFn error instead of "sent"', () => {
    ... set up state for failure ...
    dispatcher.send(...);
    ... What do I do here or how do I wrap the following? ...
    expect(onSent).not.to.have.been.called;
    expect(onError).to.have.been.called;
  });
});

如果我可以doSomethingAsync作为send的结果返回承诺,我知道如何执行此操作,但事实并非如此。我所拥有的只是知道最终会发出'发送'或'错误'事件。

我的理想语法如下:

expect(onError).to.eventually.have

但是,这不起作用。我只需将expect包装在新的承诺中,就可以获得一个非错误的版本。但我不知道为什么会这样。

  // This one works for some unknown reason!
  it('should emit "send" on send success', () => {
    ... set up state for success ...
    dispatcher.send(...);
    return Promise.resolve().then(() => {
       expect(onSent).to.have.been.called;
       expect(onError).not.to.have.been.called;
    });     
  });

如果我能以揭露内部承诺的方式重构代码,那么这将是微不足道的解决。在其他情况下,我已经做了无数次。我的问题是非常具体如何解决这个完全模式;即如何测试无法访问的承诺或异步代码的副作用,特别是在我无法重构代码以暴露承诺时,尤其是在事件发射器周围。

我至少尝试了以下内容,看看我是否可以在测试调用期望之前触发所需的发送函数来完成所有内部回调/承诺

  • 将期望包含在承诺中
  • 使用sinon.useFakeTimers
  • 控制时间
  • 在超时中包含期望
  • 以各种异步/等待模式包装发送呼叫和期望

提前致谢。

修改

好的,所以这完全是荒谬的,但这里有一个适用于已解决和被拒绝的承诺的解决方案:

it('should behave as expected already!', (done) => {
  ... set up for failure or success as desired
  dispatcher.send();
  process.nextTick(() => {
    Promise.resolve().then(() => {
      ... expectations ...
      done();
    });
  });
});

我认为这是有效的,因为(我在这里完全猜测!)我假设抛出的错误或拒绝的承诺会立即在当前的勾号中处理,而已解决的承诺会在下一个滴答中排队。所以... process.nextTick确保我们将在下一个tick中排队这个函数,允许所有捕获/错误完成,Promise.resolve确保它在任何已经排队的promises运行后排队。顺便说一句,你也可以切换顺序或nextTick和promise.resolve(),它工作得很好。

NB 如果事件是真正异步发出的(例如,在他们自己的process.nextTick中,那么你必须有一个3级嵌套。无论是Promise-nextTick-promise还是nextTick-promise- nextTick。

我的话太乱了!

...仍然比超时更好:D

2 个答案:

答案 0 :(得分:1)

由于测试不是在等待您正在测试以解决/拒绝承诺的代码块,因此断言无法正常工作。

你的分析都是正确的,但解决的方法是解决断言中的承诺。

我对您的代码进行了一些更改并使其正常运行。

我让doSomethingAsync返回承诺(我认为这是正确的解决方案)

//要测试的代码

import EventEmitter from 'events';

const doSomethingAsync = async (x) => {
  return x;
};

class Dispatcher extends EventEmitter {
  send(x) {
    return doSomethingAsync(x)
      .then((res) => {
         this.emit('sent');
      })
      .catch((err) => {
         this.emit('error', err);
      });
  }
}

export default Dispatcher;

//测试

选项1:使用等待

import sinon from 'sinon';
import { expect } from 'chai';
import Dispatcher from '../dispatcher';

describe('send', () => {
  let dispatcher;
  let onSent;
  let onError;

  beforeEach(() => {
    dispatcher = new Dispatcher();
    onSent = sinon.stub();
    onError = sinon.stub();
    dispatcher.on('sent', onSent);
    dispatcher.on('error', onError);
  });

  it('Test 1: should emit "error" on sendFn error instead of "sent"', async () => {
    await dispatcher.send('hello');
    expect(onSent.callCount).to.equal(1);
    expect(onError.callCount).to.equal(0);
  });
});

//结果

  

纱线运行规范

     

发送       ✓应该发出"错误"在sendFn错误而不是"发送"

     

1传球(1s)

     

✨在4.18s完成。

选项2:使用承诺

在测试中,您可以看到我在dispatcher.send上使用等待,或者您也可以使用以下承诺来断言

it('Test 2: should emit "error" on sendFn error instead of "sent"', () => {
  let error = false;
  dispatcher.send('hello').then(() => {
    expect(onSent.callCount).to.equal(1);
    expect(onError.callCount).to.equal(0);
  }).catch(() => {
    error = true;
  })
  expect(error).to.equal(false);
});

PS: - 如果您不希望返回承诺,那么第一个测试用例(使用等待的测试用例)将通过&第二个会失败。

答案 1 :(得分:0)

根据anoop给出的答案,您可以执行以下操作:

function sinonSetUp( eventEmitter )
{
    let onSent = sinon.stub();
    let onError = sinon.stub();
    eventEmitter.on( 'sent', onSent );
    eventEmitter.on( 'error', onError );
    return ({
        onSent: onSent,
        onError: onError,
    });
}

it('emits a sent event', async function()
{
    let dispatcher = new Dispatcher();
    let sinonHandlers = sinonSetUp(dispatcher);
    let promise = dispatcher.send();
    await promise;

    //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

    //Checking the error event:
    //The way you wish you could do it:
    /* 
    dispatcher.on('error', function(errEventVal)
    {
        expect(errEventVal).to.be.undefined; 
    }); 
    */

    //The way you have to do it:
    //args[0] gets the returned arguments, if any exist
    let errEventArgs = sinonHandlers.onError.args[0]; 
    expect(errEventArgs).to.be.undefined;

    //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

    //Checking the sent event:
    //The way you wish you could do it:
    /* 
    dispatcher.on('sent', function(sentEventVal)
    { 
        expect(sentEventVal).not.to.be.undefined; 
    }); 
    */

    //The way you have to do it:
    //If args[0] exists, then the value will be in args[0][0]
    //See the documentation for sinonjs.
    let sentEventArgs = sinonHandlers.onSent.args[0];
    expect(sentEventArgs).not.to.be.undefined;
    let sentEventVal = sentEventArgs[0];
    expect(sentEventVal).not.to.be.undefined;
});