12

I've got an async function which runs 2000ms and then it'll throw an exception. I am trying to test exactly this behaviour with Mocha / chai, but apparently I am doing it wrong.

That's what I've tried:

First:

expect(publisher.dispatchMessage<ExampleResponseMessage>(message, {}, 2 * 1000)).to.eventually.throw();

This marks the test as passed (52ms runtime) but throws an exception 2s later. So apparently it hasn't awaited the promise of that function at all.

Second:

expect(async () => {
      await publisher.dispatchMessage<ExampleResponseMessage>(message, {}, 2 * 1000);
    }).to.throw();

The test fails with: should reject a scheduled message after a predefined timeout: AssertionError: expected [Function] to throw an error at Context.mocha_1.it (test\integration\rpc-communication.spec.ts:75:16) at

Expected behaviour is that the test passed because an exception is thrown after 2000ms, which is within the given test case timeout of 4000ms.

Additional information:

This would work. The promise is rejected with an error (I can also change it to reject with a string). That should proove that dispatchMessage() is working as intended. The test case takes 2002ms and passes then.

    try {
      await publisher.dispatchMessage<ExampleResponseMessage>(message, {}, 2 * 1000);
    } catch (err) {
      expect(err).to.be.an('Error');
    }

Question:

How do I properly test if an async function throws an exception?

kentor
  • 16,553
  • 20
  • 86
  • 144

5 Answers5

10

.to.throw() shouldn't work on async function because it doesn't throw an error, it returns rejected promise.

The problem is specific to chai-as-promised. As explained in this issue, .to.eventually.throw() won't work as expected. It asserts that a promise resolves with a function that will throw error synchronously when called. This is likely not what happens in dispatchMessage.

This depends on dispatchMessage but likely should be:

expect(publisher.dispatchMessage<ExampleResponseMessage>(message, {}, 2 * 1000))
.to.be.rejected;
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Apparently this approach doesn't work either. All tests pass within 48ms. The returned Promise `dispatchMessage` will be rejected 2s after it's been called.Tslint also highlighted this line because `[tslint] unused expression, expected an assignment or function call (no-unused-expression)` – kentor Apr 23 '18 at 08:06
  • 1
    This is a common problem for Chai. Disable this rule or use https://www.npmjs.com/package/tslint-no-unused-expression-chai . That's how async (as in 'async..await') function should be tested. It's not evident from the code you've posted that dispatchMessage returns a rejected promise. Consider providing source code for dispatchMessage. – Estus Flask Apr 23 '18 at 08:10
  • thanks for that information. I've added another snippet into the original thread which should proove that `dispatchMessage` works as intended. I don't want to use try / catch every time I want to test an async function though. The code for dispatchMessage is kinda complex because it's using deferred promises (it literally converts network events to a promise but it uses a timeout). I want to test the deferred promise timeout in that case. – kentor Apr 23 '18 at 08:21
  • 1
    Can you provide source code for your spec and not truncated just snippets? You should chain a promise that `expect` returns if spec function is `async`, `await expect...` or return a promise, `return expect...`. I suppose that's the reason why you've got TSLint error. The error will appear in other Chai assertions because that's how Chai works, but for this one the expression should be used. – Estus Flask Apr 23 '18 at 08:40
  • 1
    awaiting the expect was needed, I didn't know that. Your approach does work now, thanks a ton! – kentor Apr 23 '18 at 08:45
4

Install chai plugin chai-as-promised via NPM:

npm i -D chai-as-promised @types/chai-as-promised

in test file import and use this plugin as:

import { expect, use } from 'chai';
use(require('chai-as-promised'));

Important part is keyword await before expect,and you can test for specific error message with rejectedWith:

await expect(myPromise()).to.be.rejectedWith('my Error message');

Chai supports async/await test function. To assert promise rejection, you could use rejected or rejectedWith from chai-as-promise plugin.

Because rejected return a promise, you need an await otherwise test case finishes before promise is rejected.

it('should be rejected', async () => {
  await expect(
    publisher.dispatchMessage<ExampleResponseMessage>(message, {}, 2 * 1000)
  ).to.be.rejected;
}
psulek
  • 4,308
  • 3
  • 29
  • 37
aleung
  • 9,848
  • 3
  • 55
  • 69
1

To get the other answers to work, you have to use chai-as-promised (they do not work with out-of-the-box chai).

e.g.

const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');

chai.use(chaiAsPromised);

Older answer (for historical record)

The other answers were not working for me (maybe I don't have the right Chai version?), so here's how I worked around it.

First, define this util function:

const shouldReject = async (promise:Promise<any>, errorClass?:Function|Error, message?:string):Promise<void> => {
  let error = null;
  try {
    await promise;
  }
  catch (e) {
    error = e;
  }
  expect(error).to.not.be.null;
  if(errorClass !== undefined)
    error.should.be.an.instanceOf(errorClass);
  if(message !== undefined)
    error.message.should.equal(message);
};

In your example, you would use it like:

it('should be rejected', async () => {
  await shouldReject(
    publisher.dispatchMessage<ExampleResponseMessage>(message, {}, 2 * 1000)
  );
}
Kip
  • 107,154
  • 87
  • 232
  • 265
0

The cleanest way I have found to expect rejected promises in chai and mocha is to use the special two argument version of then. This also avoids needing chai-as-promised.

await someAsyncApiCall().then(
  () => { throw new Error('Expected reject') },
  () => {}
)

The then method will call the first lambda on accept, and the second lambda on reject.

You can make this generic by declaring your own function.

async function expectReject(promise, message = 'Expected reject') {
  return promise.then(
    () => { throw new Error(message) },
    () => {}
  )
}

And then in your test

it('should reject', async ()=>{
  await expectReject(someAsyncApiCall())
})
Steven Spungin
  • 27,002
  • 5
  • 88
  • 78
0

I find all these async thrown error catching with chai over complicated.

Sometimes it's nicer to have something very basic:

it('should throw', async () => {
  try {
    await myBadAsyncFunction(someParams)
    assert.fail("Should have thrown")
  } catch (err) {
    assert.strictEqual(err.message, "This should be the error message")
  }
}

No chai involved. Very easy to read and understand.

MetaZebre
  • 769
  • 8
  • 10