Verify that an exception is thrown using Mocha / Chai and async/await
Asked Answered
U

15

85

I'm struggling to work out the best way to verify that a promise is rejected in a Mocha test while using async/await.

Here's an example that works, but I dislike that should.be.rejectedWith returns a promise that needs to be returned from the test function to be evaluated properly. Using async/await removes this requirement for testing values (as I do for the result of wins() below), and I feel that it is likely that I will forget the return statement at some point, in which case the test will always pass.

// Always succeeds
function wins() {
  return new Promise(function(resolve, reject) {
    resolve('Winner');
  });
}

// Always fails with an error
function fails() {
  return new Promise(function(resolve, reject) {
    reject('Contrived Error');
  });
}

it('throws an error', async () => {
  let r = await wins();
  r.should.equal('Winner');

  return fails().should.be.rejectedWith('Contrived Error');
});

It feels like it should be possible to use the fact that async/await translates rejections to exceptions and combine that with Chai's should.throw, but I haven't been able to determine the correct syntax.

Ideally this would work, but does not seem to:

it('throws an error', async () => {
  let r = await wins();
  r.should.equal('Winner');

  (await fails()).should.throw(Error);
});
Ukulele answered 2/8, 2017 at 16:17 Comment(2)
Try to encapsulate await fails() in a function to call it in your test and apply should.throw on this functionFroehlich
I tried this, however the await has to be encapsulated in an async function - which itself then needs to be awaited on. d'oh!Ukulele
R
118

The problem with this approach is that (await fails()).should.throw(Error) doesn't make sense.

await resolves a Promise. If the Promise rejects, it throws the rejected value.

So (await fails()).should.throw(Error) can never work: if fails() rejects, an error is thrown, and .should.throw(Error) is never executed.

The most idiomatic option you have is to use Chai's rejectedWith property, as you have shown in your question.

Here's a quick example. Not much is different from what you've demonstrated in your question; I'm just using async functions for wins() and fails() and expect instead of should. Of course, you can use functions that return a Promise and chai.should just fine.

const chai = require('chai')
const expect = chai.expect
chai.use(require('chai-as-promised'))

// Always succeeds
async function wins() {
  return 'Winner'
}

// Always fails with an error
async function fails() {
  throw new Error('Contrived Error')
}

it('wins() returns Winner', async () => {
  expect(await wins()).to.equal('Winner')
})

it('fails() throws Error', async () => {
  await expect(fails()).to.be.rejectedWith(Error)
})

If you like want your wins() test to resemble your fails() test more closely, you can write your wins() test like so:

it('wins() returns Winner', async () => {
  await expect(wins()).to.eventually.equal('Winner')
})

The key thing to remember in either of these examples is that chai-as-promised returns promises for its functions such as rejectedWith and eventually.something. Therefore you must await them in the context of an async test function, or else failing conditions will still pass:

async function wins() {
  return 'Loser'
}

async function fails() {
  return 'Winner'
}

it('wins() returns Winner', async () => {
  expect(wins()).to.eventually.equal('Winner')
})

it('fails() throws Error', async () => {
  expect(fails()).to.be.rejectedWith(Error)
})

If you ran the tests with the code above, you'd get the following:

$ npm test

> [email protected] test /home/adaline/code/mocha-chai-async
> mocha .



  √ wins() returns Winner
(node:13836) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rej
ection id: 1): AssertionError: expected 'Loser' to equal 'Winner'
(node:13836) [DEP0018] DeprecationWarning: Unhandled promise rejections are dep
recated. In the future, promise rejections that are not handled will terminate
the Node.js process with a non-zero exit code.
  √ fails() throws Error
(node:13836) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rej
ection id: 2): AssertionError: expected promise to be rejected with 'Error' but
 it was fulfilled with 'Winner'

  2 passing (11ms)

As you can see, the chai assertions actually failed, but they were failed in the context of a Promise that no one ever awaited or catched. So Mocha sees no failure and marks the tests as though they passed, but Node.js (in behaviour that will change in the future as noted above) prints the unhandled rejections to the terminal.

Rejoin answered 4/8, 2017 at 0:28 Comment(2)
Thanks, the need to await the assertion itself was the key for me.Wertz
Is there any way to log the exception?Unwelcome
L
34

I use a custom function like this:

const expectThrowsAsync = async (method, errorMessage) => {
  let error = null
  try {
    await method()
  }
  catch (err) {
    error = err
  }
  expect(error).to.be.an('Error')
  if (errorMessage) {
    expect(error.message).to.equal(errorMessage)
  }
}

and then, for a regular async function like:

const login = async (username, password) => {
  if (!username || !password) {
    throw new Error("Invalid username or password")
  }
  //await service.login(username, password)
}

I write the tests like this:

describe('login tests', () => {
  it('should throw validation error when not providing username or passsword', async () => {

    await expectThrowsAsync(() => login())
    await expectThrowsAsync(() => login(), "Invalid username or password")
    await expectThrowsAsync(() => login("username"))
    await expectThrowsAsync(() => login("username"), "Invalid username or password")
    await expectThrowsAsync(() => login(null, "password"))
    await expectThrowsAsync(() => login(null, "password"), "Invalid username or password")

    //login("username","password") will not throw an exception, so expectation will fail
    //await expectThrowsAsync(() => login("username", "password"))
  })
})
Ludly answered 13/5, 2019 at 0:29 Comment(2)
This is a great workaround due to the lack of possibilities from the libraries for async methods.Engelhart
Good one! I ended up using: const expectThrowsAsync = async (method: any, expectedError: Error) => { let actualError = null; try { await method(); } catch (err) { actualError = err; } expect(actualError).to.equal(expectedError); };Antichrist
K
16
  1. Install chai-as-promised for chai (npm i chai-as-promised -D)
  2. Just call your promise, no await should be applied!
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

chai.use(chaiAsPromised);

const expect = chai.expect;

describe('MY_DESCR', () => {
  it('MY_TEST', async () => {
    expect(myAsyncFunctionThatWillReject()).to.eventually.be.rejected; 
  });
});
Katykatya answered 31/7, 2020 at 11:24 Comment(0)
M
13

This example only works with Node!

When you use Mocha on Node.js you can use doesNotReject() or rejects() both require a function that returns a promise.


Example for when it should reject:

await rejects(testFunction());

see: https://nodejs.org/api/assert.html#assert_assert_rejects_asyncfn_error_message

Example for when it should not reject:

await doesNotReject(testFunction());

see: https://nodejs.org/api/assert.html#assert_assert_doesnotreject_asyncfn_error_message

Modifier answered 27/9, 2019 at 21:18 Comment(1)
N
7

You can use async/await and should for simple verification

it('should not throw an error', async () => {
  try {
    let r = await wins();
    r.should.equal('Winner');
  } catch (error) {
    error.should.be.null(); //should.not.exist(error) can also be used
  }
});

it('throws an error', async () => {
  let err;
  try {
    await fails();
  } catch (error) {
    err = error;
  }
    err.should.be.Error();
    err.should.have.value("message", "Contrived Error");
});
Nonet answered 13/10, 2019 at 7:54 Comment(4)
This is a bad example of testing exceptions in unit tests. If "await wins()" fails with exactly the same error message as "await fails()", then the test will pass even though "await.wins()" failed unexpectedly. At the very least split the test case into two, and test the exception in isolation.Horse
In the question above it says await wins() always succeeds so I considered the authors input as source of truth. I do agree with your point. When we use the above syntax we need to make sure both the async operations have different errors. And also I believe when you are testing for certain error you will make sure it is not being araised form the another async operation tested in the same test case.Nonet
OK, I see your point, too. Unfortunately I can't undo the -1 unless you change the answer. Rule from SO. If you care about that, I suggest that you make some small change to your answer (like splitting it into two separate test cases) and I'll be happy to undo.Horse
In both test cases here the error is checked ONLY IF the error is thrown. So for example if fails() doesn't throw an error, the test still passes because it doesn't go into the catch block.Austriahungary
L
2

If test your Promised function, in the test must wrap the code inside a try/catch and the expect() must be inside the catch error block

const loserFunc = function(...args) {
  return new Promise((resolve, rejected) => {
    // some code
    return rejected('fail because...');
  });
};

So, then in your test

it('it should failt to loserFunc', async function() {
  try {
    await loserFunc(param1, param2, ...);
  } catch(e) {
    expect(e).to.be.a('string');
    expect(e).to.be.equals('fail because...');
  }
});

That is my approach, don't know a better way.

Locket answered 14/4, 2019 at 4:37 Comment(3)
If your loserFunc does not throw an error, the test will pass as the expects won't be executed. Move the expects outside of the block to catch this condition.Goalie
You are right @BriandeHeus. This is for those case where just throw an error. In my tests i run the expect in the try block, and in the catch, just throw an error. That is because if catch get something, mean that are others problem with the logic or something else. If this isnt a good answer or my expalation are not too clear, say it to me and make an effort to be more accurate.Locket
A better approach is to call expect.fail() at the end of the try block then do expect(err).to.not.be.instanceOf(AssertionError) in the catch block.Cariecaries
Q
1

you can write a function to swap resolve & reject handler, and do anything normally

const promise = new Promise((resolve, rejects) => {
    YourPromise.then(rejects, resolve);
})
const res = await promise;
res.should.be.an("error");
Quar answered 25/11, 2019 at 9:46 Comment(0)
I
1

I have came with this solution:

import { assert, expect, use } from "chai";
import * as chaiAsPromised from "chai-as-promised";

describe("using chaiAsPromised", () => {
    it("throws an error", async () => {
        await expect(await fails()).to.eventually.be.rejected;
    });
});
Insistency answered 6/12, 2019 at 9:15 Comment(1)
Welcome to stackoverflow, and thank you for answering this question. Can you give a short explanation to go with your solution? Preferably with what the problem is you solved, and how you solved it. This will help us understand your solution better and learn from it.Gardas
C
1

On Chai I get the error Property 'rejectedWith' does not exist on type 'Assertion' for the top answer. Below is a quick solution, chai-as-promised is probably better long term.

const fail = () => { expect(true).to.eq(false) }

it('passes', async () => {
  return wins().then((res) => { expect(res).to.eq('Winner') }, fail)
})

it('throws an error', async () => {
  return fails().then(fail, (err) => { expect(err).to.eq('Contrived Error') })
})
Caecilian answered 18/9, 2022 at 4:30 Comment(0)
S
0

This is my Solution for the problem .

    try {
        // here the function that i expect to will return an errror
        let walletid = await Network.submitTransaction(transaction)
    } catch (error) {
        //  assign error.message to ErrorMessage
        var ErrorMessage = error.message;
        //  catch it and  re throw it in assret.throws fn and pass the error.message as argument and assert it is the same message expected
        assert.throws(() => { throw new Error(ErrorMessage) },'This user already exists');
    }
    // here assert that ErrorMessage is Defined ; if it is not defined it means that no error occurs
    assert.isDefined(ErrorMessage);
Samurai answered 28/1, 2019 at 23:55 Comment(0)
F
0

A no dependency on anything but Mocha example.

Throw a known error, catch all errors, and only rethrow the known one.

  it('should throw an error', async () => {
    try {
      await myFunction()
      throw new Error('Expected error')
    } catch (e) {
      if (e.message && e.message === 'Expected error') throw e
    }
  })

If you test for errors often, wrap the code in a custom it function.

function itThrows(message, handler) {
  it(message, async () => {
    try {
      await handler()
      throw new Error('Expected error')
    } catch (e) {
      if (e.message && e.message === 'Expected error') throw e
    }
  })
}

Then use it like this:

  itThrows('should throw an error', async () => {
    await myFunction()
  })
Favourable answered 18/11, 2019 at 14:49 Comment(0)
A
0

Another way (applicable to an async function, but not using await in the test) is calling done with an assertion error:

it('should throw Error', (done) => {
  myService.myAsyncMethod().catch((e) => {
    try {
      // if you want to check the error message for example
      assert.equal(e.message, 'expected error');
    } catch (assertionError) {
      done(assertionError); // this will fail properly the test
      return; // this prevents from calling twice done()
    }

    done();
  });
});
Antimissile answered 24/7, 2020 at 8:14 Comment(0)
S
0

Add this at the top of your file:

import * as chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

chai.use(chaiAsPromised)

Then the assert should be this way:

await expect(
    yourFunctionCallThatReturnsAnAwait()
).to.eventually.be.rejectedWith("revert"); // "revert" in case of web3
Sachs answered 26/9, 2021 at 13:42 Comment(0)
D
0

Here's a TypeScript implementation of solution:

import { expect } from 'chai';

export async function expectThrowsAsync(
  method: () => Promise<unknown>,
  errorMessage: string,
) {
  let error: Error;
  try {
    await method();
  } catch (err) {
    error = err;
  }
  expect(error).to.be.an(Error.name);
  if (errorMessage) {
    expect(error.message).to.equal(errorMessage);
  }
}

Inspired by solution from @kord

Dreamland answered 21/1, 2022 at 20:32 Comment(0)
C
0

I tried every answer on this page but I didn't really wanted to install extra chai packages only to catch one single error in one single test. So I adopted this way which seems to work smoothly with a basic chai setup:

try {
    await mockClass.errorPromise();
}
catch (error: any) {
    expect(error).to.be.instanceof(Error);
    expect(error.message).to.equal("Argument is required");
}
Carolincarolina answered 8/8, 2023 at 9:12 Comment(2)
That is not a recommended approach since you can have false positives In cases where the promise does not fail the expect will never be executedLizalizabeth
Very true. Forgot to add, on the tests I was working with here I had another dozen of other cases that will catch that specific scenario, this answer is oversimplified, in reality that try/catch had more other lines inside and around of it.Carolincarolina

© 2022 - 2024 — McMap. All rights reserved.