Handling async behaviour in React component testing with Jest

Image for post
Image for post

I can guess why you’re here. You started testing your React components with Jest. Awesome! 😎 But then you started adding async behaviour to your components and now you started seeing these vague “fail” messages:

Image for post
Image for post

In today’s post, I’ll give you some recipes for common testing scenarios that deal with sync and async component update behaviour, which include:

  • How to test sync updates to a component state (using act)
  • How to await something that we have control over (using wait)
  • How to await something that we don’t have control over (using waitFor)

We will use react-test-render to provide a lower-level understanding, but know that the test custom helpers that we’ll write often already exist in higher-level libraries such as react-testing-library.

Case 1: Test assertions on sync updates

Before we dive into async behaviour, it’s helpful to understand how sync updates work in our React testing environment.

Without any additional wrapping, you’ll find your components will encounter a race condition while trying to make test assertions on their the final state. As seen below, our expect assertion can’t find “render 2” since it is being made while the component is still updating.

Image for post
Image for post

To prevent this limbo state, React encourages us to use an act(...) method, which has the purpose of ensuring that state updates to the component have finished before we run our assertions.

Image for post
Image for post

With our wrapping of our state mutation inside an act, our test assertion will now be able to find “render 2” without any complaints from the testing engine.

Case 2: Test assertions on async updates that we have control over

When async behaviour is added to our components, using an act becomes slightly more complicated.

Image for post
Image for post

Consider the case above where we mock that an async mutation is called upon a button press. Despite wrapping our update in an act, the async side effect would not be handled within our act since it exists within an outside context. This is likely what you’ve experienced recently, which has led you here.

Image for post
Image for post

We can capture the final render state of our component by creating a function called wait(). This function’s responsibility is to add an empty promise to the end of the execution queue, which we can then await within our act to ensure that the component has finished all side effects.

Case 3: Test assertions on async updates that we don’t have control over

There may be some cases where we need to await things that are not explicit, such as a side-effect from a useEffect hook or an animated dropdown.

This is a situation that could benefit from us writing a function called waitFor(), which has the responsibility of polling the state of the testing environment with our set of assertions.

Image for post
Image for post

While this method is still effective, it is certainly less elegant and should only be used as a last resort when it is impossible to effectively handle your side effects using an act() or wait().

Final thoughts

I hope that these recipes can unblock some of the frustrations that you’ve been encountering with async testing. I know the pain myself.

Remember that writing advanced testing utilities are not always the answer. If you find yourself trying to climb too big of a hill, it’s probably a good signal that you need to simplify your component structure into something that can be tested with elegance and ease.

Software Developer from Canada 🇨🇦

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store