【问题标题】:What's the best way to unit test an event being emitted in Nodejs?对 Nodejs 中发出的事件进行单元测试的最佳方法是什么?
【发布时间】:2013-05-25 10:46:43
【问题描述】:

我正在编写一堆 mocha 测试,我想测试是否发出了特定事件。目前,我正在这样做:

  it('should emit an some_event', function(done){
    myObj.on('some_event',function(){
      assert(true);
      done();
    });
  });

但是,如果该事件从不发出,它会使测试套件崩溃,而不是使该测试失败。

最好的测试方法是什么?

【问题讨论】:

  • 代码如何“使测试套件崩溃”?我希望这个特定的测试会超时。可能其他部分测试代码有问题。

标签: javascript node.js mocha.js


【解决方案1】:

如果您可以保证事件应在一定时间内触发,则只需设置超时。

it('should emit an some_event', function(done){
  this.timeout(1000); //timeout with an error if done() isn't called within one second

  myObj.on('some_event',function(){
    // perform any other assertions you want here
    done();
  });

  // execute some code which should trigger 'some_event' on myObj
});

如果您不能保证事件何时触发,那么它可能不适合进行单元测试。

【讨论】:

  • 没有必要whatsoever使用setTimeout自己设置超时。 Mocha 有一个超时功能。 assert(true) 100% 没用。摩卡甚至不会知道它发生了。如果你想让测试失败,你可以抛出任何你想要的旧异常。无需使用assert(false)。但在这种情况下,Ollie Ford 已经指出了一个更好的方法:done(new Error(...))
  • @Louis 看起来更好吗?
  • 是的,尽管现在我认为这个问题很糟糕。我之前实际上并没有仔细研究它,但是正如对问题的评论所表明的那样,OP 显示的测试应该只是超时而不是“崩溃”。这里的答案在功能上与 OP 所做的没有什么不同。你有this.timeout(1000),但 Mocha 的默认超时为 2000,所以我们永远不会在没有超时的情况下运行。这个问题的所有答案要么在做 OP 已经在做的事情,要么包括不必要的扭曲来试图解决不存在的问题。
  • @Louis 这是一个很好的观点,尽管可能不值得担心 OP 的意图,因为这个问题已经存在三年了。
【解决方案2】:

9 月 30 日编辑:

我看到我的答案被接受为正确答案,但是 Bret Copeland 的技术(见下面的答案)更好,因为它在测试成功时更快,大多数情况下你运行测试作为其中的一部分一个测试套件。


Bret Copeland 的技术是正确的。你也可以做一些不同的事情:

  it('should emit an some_event', function(done){
    var eventFired = false
    setTimeout(function () {
      assert(eventFired, 'Event did not fire in 1000 ms.');
      done();
    }, 1000); //timeout with an error in one second
    myObj.on('some_event',function(){
      eventFired = true
    });
    // do something that should trigger the event
  });

Sinon.js 的帮助下,这可以缩短一点。

  it('should emit an some_event', function(done){
    var eventSpy = sinon.spy()
    setTimeout(function () {
      assert(eventSpy.called, 'Event did not fire in 1000ms.');
      assert(eventSpy.calledOnce, 'Event fired more than once');
      done();
    }, 1000); //timeout with an error in one second
    myObj.on('some_event',eventSpy);
    // do something that should trigger the event
  });

在这里,我们不仅要检查是否触发了事件,还要检查是否在超时期间仅触发了一次 if 事件。

Sinon 还支持calledWithcalledOn,以检查使用了哪些参数和函数上下文。

请注意,如果您希望事件与触发事件的操作同步触发(两者之间没有异步调用),那么您可以将超时设置为零。 1000 毫秒的超时仅在您执行异步调用时才需要,这需要很长时间才能完成。很可能不是这样。

实际上,当事件保证与导致它的操作同步触发时,您可以将代码简化为

  it('should emit an some_event', function() {
    eventSpy = sinon.spy()
    myObj.on('some_event',eventSpy);
    // do something that should trigger the event
    assert(eventSpy.called, 'Event did not fire.');
    assert(eventSpy.calledOnce, 'Event fired more than once');
  });

否则,Bret Copeland 的技术在“成功”情况下(希望是常见情况)总是更快,因为如果事件被触发,它能够立即调用 done

【讨论】:

  • 我只想指出,这样做的缺点是测试总是需要一秒钟(或任何超时时间),即使事件仅在 3 毫秒内触发。如果您正在运行大量此类样式测试,这可能是一个很大的劣势,因为它可能导致它们运行速度慢数百倍。您可以使用更短的超时时间,但会根据事件的性质增加“误报”的风险。所以这真的是一个个案决定。
  • 布雷特,好点子。我居然没有意识到这个区别!你的解决方案显然是优越的。我发布这个主要是为了演示 Sinon,因为这在测试回调时使用了很多。在这种情况下,它可能不太合适。此外,如果多次调用 done,Mocha 会自动测试不触发多次的事件。
  • 是的,您发布的所有内容都是有效且值得赞赏的。我只是想指出这种差异。
  • 我更新了我的答案,包括一个同步触发事件的示例,以使其更有价值。
【解决方案3】:

这种方法确保了最短的等待时间,但最大的机会由套件超时设置,并且非常干净。

  it('should emit an some_event', function(done){
    myObj.on('some_event', done);
  });

也可以用于 CPS 风格的功能...

  it('should call back when done', function(done){
    myAsyncFunction(options, done);
  });

这个想法还可以通过在done 周围放置一个包装器来扩展以检查更多细节 - 例如参数和this。例如,感谢this answer 我可以做到...

it('asynchronously emits finish after logging is complete', function(done){
    const EE = require('events');
    const testEmitter = new EE();

    var cb = sinon.spy(completed);

    process.nextTick(() => testEmitter.emit('finish'));

    testEmitter.on('finish', cb.bind(null));

    process.nextTick(() => testEmitter.emit('finish'));

    function completed() {

        if(cb.callCount < 2)
            return;

        expect(cb).to.have.been.calledTwice;
        expect(cb).to.have.been.calledOn(null);
        expect(cb).to.have.been.calledWithExactly();

        done()
    }

});

【讨论】:

  • 是的,这正是它应该做的。没有理由使用setTimeout
  • 我没有看到做process.nextTick 的意义。如果您在第一个 process.nextTick 之前移动 testEmitter.on,则直接调用 testEmitter.emit('finish') 一切都会正常工作。
  • @Louis 只是为了模拟实际情况。我正在测试一个在完成任务后发出事件的对象。有时任务是同步的,有时不是,但对象必须始终异步运行,这是我正在测试的特征之一。如果是同步返回,上面的测试就会失败。我也可以通过在调用对象后订阅事件来做到这一点,但我这样做是为了测试,一般来说,调用是在订阅之前还是之后都没有关系。
  • 感谢您花时间写下捕获回调的方法。
【解决方案4】:

坚持:

this.timeout(<time ms>);

在您的 it 语句的顶部:

it('should emit an some_event', function(done){
    this.timeout(1000);
    myObj.on('some_event',function(){
      assert(true);
      done();
    });`enter code here`
  });

【讨论】:

    【解决方案5】:

    在这里聚会迟到了,但我正面临这个问题并想出了另一个解决方案。 Bret 接受的答案是一个很好的答案,但我发现它在运行我的完整 mocha 测试套件时造成了严重破坏,引发了错误done() called multiple times,我最终放弃了尝试进行故障排除。 Meryl 的回答让我走上了我自己的解决方案的道路,该解决方案也使用sinon,但不需要使用超时。通过简单地存根emit() 方法,您可以测试它是否被调用并验证它的参数。这假设您的对象继承自 Node 的 EventEmitter 类。 emit 方法的名称可能与您的情况不同。

    var sinon = require('sinon');
    
    // ...
    
    describe("#someMethod", function(){
        it("should emit `some_event`", function(done){
            var myObj = new MyObj({/* some params */})
    
            // This assumes your object inherits from Node's EventEmitter
            // The name of your `emit` method may be different, eg `trigger`  
            var eventStub = sinon.stub(myObj, 'emit')
    
            myObj.someMethod();
            eventStub.calledWith("some_event").should.eql(true);
            eventStub.restore();
            done();
        })
    })
    

    【讨论】:

    • 这很有趣,但我正在尝试测试 DOM 事件。对这可能如何工作的想法?
    • 我对前端单元测试不太熟悉,但我认为同样的方法也可以。这将取决于您的活动的具体背景。您只需存根与emit 等效的任何方法。所以对于 jQuery,我认为你会做类似 var $el = $("#someElement"); var eventStub = sinon.stub($el, 'trigger'); 这完全是推测性的和未经测试的,但希望能帮助你找到一个可行的解决方案。
    【解决方案6】:

    更好的解决方案而不是 sinon.timers 是使用 es6 - Promises

    //Simple EventEmitter
    let emitEvent = ( eventType, callback ) => callback( eventType )
    
    //Test case
    it('Try to test ASYNC event emitter', () => {
      let mySpy = sinon.spy() //callback
      return expect( new Promise( resolve => {
        //event happends in 300 ms
        setTimeout( () => { emitEvent('Event is fired!', (...args) => resolve( mySpy(...args) )) }, 300 ) //call wrapped callback
      } )
      .then( () => mySpy.args )).to.eventually.be.deep.equal([['Event is fired!']]) //ok
    })
    

    如你所见,关键是用resolve包裹回调:(... args) => resolve (mySpy (... args))

    因此,PROMIS new Promise().then() 在被调用回调之后被解析。

    但是一旦回调被调用,你就可以测试你对他的期望了。

    优势

    • 我们不需要猜测超时来等待事件被触发(在许多 describe() 和 its() 的情况下),而不取决于计算机的性能
    • 测试会更快通过

    【讨论】:

    • 在某些情况下,是的,承诺是要走的路,但在这里使用承诺没有任何好处。这样做只会为解决方案添加不必要的机制。
    【解决方案7】:

    我通过将事件包装在 Promise 中来做到这一点:

    // this function is declared outside all my tests, as a helper
    const waitForEvent = (asynFunc) => {
        return new Promise((resolve, reject) => {
            asyncFunc.on('completed', (result) => {
                resolve(result);
            }
            asyncFunc.on('error', (err) => {
                reject(err);
            }
        });
    });
    
    it('should do something', async function() {
        this.timeout(10000);  // in case of long running process
        try {
            const val = someAsyncFunc();
            await waitForEvent(someAsyncFunc);
            assert.ok(val)
        } catch (e) {
            throw e;
        }
    }
    

    【讨论】:

      【解决方案8】:

      我建议使用once() 以获得更简单的解决方案,尤其是如果您喜欢 async/await 风格:

      const once = require('events').once
      // OR import { once } from 'events'
      
      it('should emit an some_event', async function() {
          this.timeout(1000); //timeout with an error if await waits more than 1 sec
      
          p = once(myObj, 'some_event')
          // execute some code which should trigger 'some_event' on myObj
          await p
      });
      

      如果您需要检查值:

      [obj] = await p
      assert.equal(obj.a, 'a')
      

      最后,如果你使用的是 typescript,下面的 helper 可能会很方便:

      // Wait for event and return first data item
      async function onceTyped<T>(event: string): Promise<T> {
          return <T>(await once(myObj, event))[0]
      }
      

      这样使用:

       const p = onceTyped<SomeEvent>(myObj, 'some_event')
       // execute some code which should trigger 'some_event' on myObj
       const someEvent = await p // someEvent has type SomeEvent
       assert.equal(someEvent.a, 'a')
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-09-27
        • 2019-02-12
        • 1970-01-01
        • 2010-09-07
        • 1970-01-01
        • 2011-06-06
        • 2011-03-05
        • 1970-01-01
        相关资源
        最近更新 更多