Skip to main content

Command Palette

Search for a command to run...

Demystifying Jest Async Testing Patterns

Published
5 min readView as Markdown
Demystifying Jest Async Testing Patterns
L

Known for his open source and JavaScript security initiatives, Liran Tal is an award-winning software developer, security researcher, and open source champion in the JavaScript community. He's an internationally recognized GitHub Star, acknowledged for his open source advocacy, and has received the OpenJS Foundation's Pathfinder for Security for his work on Node.js security. His contributions to developer security education include leading OWASP projects, building supply chain security tools, participation in CNCF and OpenSSF initiatives, and authoring books such as O'Reilly's Serverless Security. He leads the developer advocacy team at Snyk.io and is on a mission to empower developers with better application security skills.

Aug 9, 2018 ~ 4 min read

Demystifying Jest Async Testing Patterns

share this story on

There are several traps that are easy to fall to when it comes to async testing. Moreover, there are several methods of achieving the same…

There are several traps that are easy to fall to when it comes to async testing. Moreover, there are several methods of achieving the same thing depending on your flavor.

An important insight a developer can possess is what bad practices NOT to follow and identifying bad code patterns.

In this post I’d like to demystify some of these async patterns and highlight the traps that are hidden inside.

Expecting Promises

Imagine a world without Async/Await.

In the following use-cases we have an async function somethingAsync that does some business logic which we want to test and returns a promise, maybe we also stub some of it, but that’s not really the point.

One pattern of assertions can be to call this stub or actual subject under test and once the promise resolves we chain it to a thenable that allows to assert on the result using Jest’s custom matchers.

Here is an example:

The thing is , the above test is not written well — It will always pass.

Because we aren’t returning the promise from the test then Jest has no idea that this test is asynchronous so it just calls the promise and continues on. Since no expectations triggered any errors the test pass.

The semantics are important to understand:

  • As it is, with somethingAsync rejecting the promise the test itself will pass and there will be a warning for an unhandled promise rejection since there’s nothing to catch it.
  • If you change the return value of somethingAsync to resolve instead of rejecting, then something even worse happens — the expect is never reached and you get a false positive with no indication that the test is not really testing anything.

There’s another variation of the above and that is to wrap any promise with expect and use its built-in matchers to assert on the return value.

It looks like this:

It’s easy to fall into the trap of this methodology as well because we’re used to asserting data structures or possibly synchronous function calls.

So the above snippet is also broken — While the expectation is called, there is no way to assert on its return value since the test code has already ended.

The outcome of the above snippet will be either a blindly passing test, or a test that passes with additional log output due to the unhandled promise rejection and the missed expectation.

It looks something like this:

The solution for both of the methods laid out above are to return the promise as can be seen in the following example:

When Errors Go Lost

We’re back to our Async/Await world. Yay!

In the following use-case we are hoping to drive the application towards throwing an error and rejecting the promise and we want to catch it and match the error message.

A tempting approach is to catch the error thrown, since you know that getUserName is going to throw, and assert the exact error object and message:

There’s a very common error here though, and that’s the fact that if the getUserName() async function would have been refactored in a way that would actually resolve the promise then the test would blindly pass, therefore rendering this test useless and providing a false positive where it should’ve failed.

If you’re keen on the try/catch block, one way to deal with the above problem is to declare an expected assertion count as follows:

2 changes in the above code snippet are:

  1. We updated the getUserName() to resolve in order to simulate a code refactoring that changed the logic.
  2. We added an expected assertion count to the test itself

The above test is going to end with no assertions made due to the catch block not being reached. Jest will then fail the test as it missed the expected assertions count.

Explicit Expectations

I find assertions count somewhat non-elegant.
Fortunately, there’s another way.

Jest has matchers for promises that can assert a resolved or rejected promise.
I find this way to be more explicit and self-explaining on what the test is doing or expecting.

Let’s change the above test:

You should still remember the golden rules of testing asynchronous code — always return a promise (return the expect) or make sure to await the expectation to unwrap the promise’s return value.

Expecting Assertions

A final note on when to use and when to avoid assertions planing (based off of Ava’s reference):

Avoid expect.assertions(N) when:

  1. Your tests are synchronous
  2. You are using promises (in which case, just return the expectation)
  3. You are using async/await with try/catch (again, just await the expectation)

Use expect.assertions(N) when:

  1. Your test has conditionals — and therefore, you may have in one branch of code one expectation, and in the other several. This makes it impossible to plan your exact assertions count, but luckily you can use the expect.hasAssertions() to verify that at least one assertion has been made.
  2. Your asynchronous test code uses callbacks — if you are asserting inside those callbacks then you want to make sure you define your expected assertions so that you can be sure the callback was indeed called and asserted.

Happy Jesting! 😋

More from this blog

Liran Tal's blog

178 posts

Author of Node.js Secure Coding, Awarded GitHub Star and OpenJS Foundation's Pathfinder Award for Security. Security researcher, advocate for open source, web security and kindness.