Is there a way to get Chai working with asynchronous Mocha tests?

Thomas Parslow picture Thomas Parslow · Jun 28, 2012 · Viewed 48.8k times · Source

I'm running some asynchronous tests in Mocha using the Browser Runner and I'm trying to use Chai's expect style assertions:

window.expect = chai.expect;
describe('my test', function() {
  it('should do something', function (done) {
    setTimeout(function () {
      expect(true).to.equal(false);
    }, 100);
  }
}

This doesn't give me the normal failed assertion message, instead I get:

Error: the string "Uncaught AssertionError: expected true to equal false" was thrown, throw an Error :)
    at Runner.fail (http://localhost:8000/tests/integration/mocha/vendor/mocha.js:3475:11)
    at Runner.uncaught (http://localhost:8000/tests/integration/mocha/vendor/mocha.js:3748:8)
    at uncaught (http://localhost:8000/tests/integration/mocha/vendor/mocha.js:3778:10)

So it's obviously catching the error, it's just not displaying it correctly. Any ideas how to do this? I guess I could just call "done" with an error object but then I lose all the elegance of something like Chai and it becomes very clunky...

Answer

Jean Vincent picture Jean Vincent · Mar 4, 2013

Your asynchronous test generates an exception, on failed expect()ations, that cannot be captured by it() because the exception is thrown outside of it()'s scope.

The captured exception that you see displayed is captured using process.on('uncaughtException') under node or using window.onerror() in the browser.

To fix this issue, you need to capture the exception within the asynchronous function called by setTimeout() in order to call done() with the exception as the first parameter. You also need to call done() with no parameter to indicate success, otherwise mocha would report a timeout error because your test function would never have signaled that it was done:

window.expect = chai.expect;

describe( 'my test', function() {
  it( 'should do something', function ( done ) {
    // done() is provided by it() to indicate asynchronous completion
    // call done() with no parameter to indicate that it() is done() and successful
    // or with an error to indicate that it() failed
    setTimeout( function () {
      // Called from the event loop, not it()
      // So only the event loop could capture uncaught exceptions from here
      try {
        expect( true ).to.equal( false );
        done(); // success: call done with no parameter to indicate that it() is done()
      } catch( e ) {
        done( e ); // failure: call done with an error Object to indicate that it() failed
      }
    }, 100 );
    // returns immediately after setting timeout
    // so it() can no longer catch exception happening asynchronously
  }
}

Doing so on all your test cases is annoying and not DRY so you might want to provide a function to do this for you. Let's call this function check():

function check( done, f ) {
  try {
    f();
    done();
  } catch( e ) {
    done( e );
  }
}

With check() you can now rewrite your asynchronous tests as follows:

window.expect = chai.expect;

describe( 'my test', function() {
  it( 'should do something', function( done ) {
    setTimeout( function () {
      check( done, function() {
        expect( true ).to.equal( false );
      } );
    }, 100 );
  }
}