Some thoughts about unit test in React

date
Dec 9, 2021
status
Published
tags
react
unit test
summary
type
Post
  1. Do not wrap fireEvent in “act” or “await waitFor”
If we are testing with fireEvent in react-testing library but found this error in the console
It means that we did not wait for react to flush the new state to the dom. The app’s state is updated but we did not test it. There is a potential scenario we need to consider/test (that is the DOM is now changed as the state is changed, do you expect it to change?).
Here is an example that replicates this issue:
notion image
In this example, we fired a click event and immediately did the assertion. But the state will only be updated after 2s later. Even if the test is pass, we got the “not wrapped in act” warning in the console.
notion image
A wrong test that does not wait for react to update the state
The assertion is wrong. We are supposed to wait for react to flush the state and after the state is updated, then we do the assertion.
So in this example, ‘await waitFor’ is the rescue.
notion image
when to use wait waitFor
This time it passed without any warnings. Whenever we fired an event, ask ourselves will it trigger any react state updates. If the answer is YES, then always use “await waitFor” to wait for the state to update.
In this example, it is not best practice to really wait for 2s, which is too slow. We could manipulate the time by using jest.useFakeTimers and jest.advanceTimersByTime(2000).
2. fire event with userEvent instead of fireEvent
fireEvent cannot simulate users’ real behaviors. When you fire a click event with fireEvent.click(targetButton), it simply clicks that element, but the button is not focused. However, when the user clicks a button, the button should also be focused.
This example replicates this issue:
notion image
fireEvent.click vs userEvent.click
fireEvent.click only fires click event but the focus event is not fired, which is not correct in production environment. userEvent.click fixes this problem. It is just a wrapper of fireEvent but can better simulate users’ behaviors.
This focus issue is discussed here:
Recommended to read:
Here is the example code from codesandbox:

Other things about unit testing:

1. Passing functions as parameters are better than call them directly in another function.
If you are familiar with dependency injection in angular or c#, you already know that the biggest benefit of Di is that it makes your code easier to test.
Let use check out this example:
notion image
example of passing function as dependencies
In this example, we have a ‘displayValueInCapital’ function that returns a capital value, a ‘findName’ function that returns the target object, and a ‘renderName’ function that accepts a list, target string, the findName function and displayValueInCapital function.
We pass findNameFunc and displayValueFunc as dependencies and call them in ‘renderName’. It is much better than directly call them inside of renderName and makes these three functions less coupled.
If we already have tests for ‘displayValueInCapital’ and ‘findName’, then it is unnecessary to test them again in renderName. So in renderName, we can just mock these two functions like this:
notion image
mock function dependencies
The first two tests already tested displayValueInCapital and findName functions. In the last test, we can easily mock the dependencies and only test the core logic that is related to renderName.
However, if we just call them directly in renderName like this:
notion image
a bad example of renderName
it makes the renderName2 too coupled with findName and displayValueInCapital and not that easy to mock these two functions.
2. Do not use Optional chaining(?.) all the time
By using optional chaining (?.), it means there are more branches to test. One branch is that the value exists, and other branch is that the value does not exist. Additionally, errors or issues could be hidden and hard to find.

© ming 2021 - 2025