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:
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.
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.
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.
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.
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.
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().
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.