Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Event-based unit tests fail with "done() called multiple times"

I have a simple async callback test that I've set up with mocha:

describe('test', function () {
    it('should not work', function(done) {
        client.on('success', function () {
          return done('client saw success message but should have errored');
        });
        client.on('error', function (err) {
          return done();
        });
    });
});

The idea is that the client does some async operation and should receive an error event. If it receives anything else, then the test should fail.

Unfortunately, mocha keeps complaining:

done() called multiple times

I've done all sorts of things to verify that this is not true. For example, I've tried throwing an error before the done in the success handler, logging when control reaches the success handler, etc.

How can I get this test to run without telling me I'm calling done twice? I would throw an error instead of calling done with an error message, but that would cause the test to fail with a timeout instead of the error I want.

like image 871
nmagerko Avatar asked Nov 12 '17 18:11

nmagerko


Video Answer


1 Answers

Your tests are failing because you are still listening for events after the test is finished.

A completed test doesn't automatically remove the event listeners.

On your next test you are firing the event again but previous test event listeners are called again since they are still listening for the event. Since done was already called on them when that test completed, they fire again hence you get the error that done was called multiple times.

Couple of options here:

  • You can remove the event listeners after each test by using a named function.
  • You can use the once listener.

Removing event listeners via a named function:

describe('test', () => {  
  it('should work', done => {
    const finish = err => {
      done(err)
      client.removeListener('success', finish)
      client.removeListener('error', finish)
    }

    client.on('error', finish)    
    client.on('success', result => {
      result.should.equal('foo')
      // rest of tests for result...

      finish()
    })

    client.fireEvent()
  })  
})

Note that you might need to use off or removeEventListener instead of removeListener - whichever method your client uses to remove a listener.

Using the once listener:

Alternatively, you can use the once listener to listen for events. Like the name suggests, this handler fires only once so there's no need to manually remove listeners afterwards.

describe('test', function () {  
  it('should work', done => {

    client.once('error', done)
    client.once('success', result => {
      result.should.equal('foo')
      // rest of tests for result...

      done()
    })

    client.fireEvent()
  })
})

Warning: These methods have an important caveat. They don't allow you to test the edge case on whether your client actually fires the event just once. If client erroneously fires success more than once your test would erroneously succeed as well. I'm not sure how you can handle this gracefully at this time but comments are welcome.

like image 66
nicholaswmin Avatar answered Sep 20 '22 02:09

nicholaswmin