Testing promise rejection in JavaScript with Jest

The code

Let’s consider a simple function that returns a Promise that can either resolve or reject depending on the value of
the first argument:

export default function promiseMe(result, timeout = 1000) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (result instanceof Error || result.startsWith("Error")) {
                reject(result)
            } else {
                resolve(result)
            }
        }, timeout)
    })
}

While testing async code with Jest the only thing to remember is to return Promise from the test so that Jest can
wait for it to resolve or to reject. The cleanest way is to do it with .resolves matcher:

const successMessage = "Done.";

// with async/await
it("resolves (1)", async () => {
    await expect(promiseMe(successMessage)).resolves.toEqual(successMessage);
});

// without async/await
it("resolves (2)", () => {
    return expect(promiseMe(successMessage)).resolves.toEqual(successMessage);
});

In case the Promise rejects and the test did not expect that, Jest reports an error:

Error: expect(received).resolves.toEqual()

Received promise rejected instead of resolved
Rejected to value: [...]

But what if one want to test Promise rejection and verify the rejection reason?

Try-catch with async/await (bad)

It looks like using try-catch with async/await is the easiest way to achieve this as the rejected value is thrown:

it("rejects (bad)", async () => {
    try {
        await promiseMe("Error");
    } catch (e) {
        expect(e).toEqual("Error");
    }
});

But wait. What happens when the Promise returned by promiseMe function won’t reject, but it resolves instead? Well, the test still passes, as the catch block is never reached.

See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#promise_rejection

Try-catch with async/await (better)

To overcome that issue, one could expect that the actual assertion will be executed and fail the test if it does not happen.
This can be done pretty easily with expect.assertions which verifies that a certain number of assertions are called
during a test:

it("rejects (better)", async () => {
    expect.assertions(1);
    try {
        await promiseMe("Error");
    } catch (e) {
        expect(e).toEqual("Error");
    }
});

Now, when there is no rejection, the test fails:

Error: expect.assertions(1)

Expected one assertion to be called but received zero assertion calls.

.rejects (best)

To make the code even more expressive .rejects matcher can be used:

it("rejects (best)", async () => {
    await expect(promiseMe("Error")).rejects.toEqual("Error");
});

When there is no rejection, Jest reports an error:

Error: expect(received).rejects.toEqual()  
  
Received promise resolved instead of rejected  
Resolved to value: [...]  

If the rejected value is an Error object, toThrow matcher can be used:

it("rejects (best)", async () => {
    await expect(promiseMe(new Error(errorMessage))).rejects.toThrow(errorMessage);
    await expect(promiseMe(new Error(errorMessage))).rejects.toThrow(Error); // type check
    await expect(promiseMe(new Error(errorMessage))).rejects.toThrow(new Error(errorMessage));
});

See also

Popular posts from this blog

Parameterized tests in JavaScript with Jest

macOS: Insert current date shortcut with `Shortcuts.app`