web-dev-qa-db-ja.com

Nodejsで発行されるイベントを単体テストする最良の方法は何ですか?

私はたくさんのモカテストを書いていますが、特定のイベントが発生することをテストしたいと思います。現在、私はこれをやっています:

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

ただし、イベントが発行されない場合、その1つのテストに失敗するのではなく、テストスイートがクラッシュします。

これをテストする最良の方法は何ですか?

27
manalang

特定の時間内にイベントが発生することを保証できる場合は、単にタイムアウトを設定します。

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
});

イベントがいつ発生するかを保証できない場合は、単体テストの適切な候補ではない可能性があります。

34
Bret Copeland

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
  });

ここでは、イベントが発生しただけでなく、イベントがタイムアウト期間中に1回だけ発生したかどうかもチェックしています。

Sinonは、calledWithおよびcalledOnもサポートしており、どの引数と関数コンテキストが使用されたかを確認します。

イベントをトリガーした操作と同期してイベントがトリガーされることを期待している場合(間に非同期呼び出しがない場合)、タイムアウトを0にして実行できることに注意してください。 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を呼び出すことができるため、「成功」の場合(できれば一般的な場合)で常に高速です。

14
Myrne Stol

この方法により、最小限の待機時間が保証されますが、スイートのタイムアウトによって設定された最大の機会が確保され、非常にきれいになります。

  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);
  });

thisの周りにラッパーを配置することにより、引数やdoneなどの詳細を確認するためにアイデアを拡張することもできます。たとえば、 この答え おかげで...

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()
    }

});
5
Cool Blue

ただスティック:

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`
  });
3
Pete

ここでパーティーに遅れましたが、私はまさにこの問題に直面していて、別の解決策を思いつきました。 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();
    })
})
2
Ben

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;
    }
}
0
MFB

より良い解決策 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
})

ご覧のとおり、キーバックは解決でカルバックをラップすることです:(... args> resolve(mySpy(... args)))==.

したがって、PROMIS new Promise()。then()は解決されます[〜#〜] only [〜#〜]はコールバックと呼ばれます。

しかし、コールバックが呼び出されると、彼に期待することを既にテストできます。

利点

  • イベントが発生するまで待機するタイムアウトを推測する必要はありません(多くのdescribe()およびits()の場合)、コンピューターのパフォーマンスに依存しません
  • テストはより速く合格します
0
meugen