SoFunction
Updated on 2025-04-07

How to unit test react hooks

Write in front

I have been using react hooks to do new projects for the company for a while, and I have stepped on a lot of pitfalls, big and small. Since it is a company project, unit tests must be written to ensure the correctness of the business logic and the maintainability and stability of the code during reconstruction. The previous project used react@ version, and it was stress-free to use enzyme and jest to do unit tests, but the new project used [email protected]. When writing unit tests, it encountered many obstacles, so summarizing this article is considered to be a sharing of experience.

Cooperate with Enzyme to test

First of all, for enzyme's support for hooks, you can refer to this issue. There are links and explanations for the support for each hook, so I won't go into details here. What I want to say here is to use Enzyme to test some changes in hooks in terms of testing and verification.

Test status

Since function component does not have an instance concept, we cannot directly verify the state in a similar way, such as:
For the count here, we cannot access the API in Enzyme, but we can use it to retrieve the text node of the button and indirectly test the count status, such as:

const Counter = () => {
 const [count, setCount] = useState(0)
 return <button>{count}</button>
}

Test Method

Similarly, we cannot directly obtain the method of component instance through the method, and then call and test, such as:

const wrapper = mount(<Counter/>)
expect(('button').text()).toBe('0')

How to get a reference to the inc method? We can save the country through:

const Counter = () => {
 const [count, setCount] = useState(0)
 const inc = useCallback(() => setCount(c => c + 1), [])
 return <button onClick={inc}>{count}</button>
}

In addition, in some cases, we expose some states and methods in hooks in a return value. If this is the case, it will be easier. You can test by writing a Wrapper component or directly using the tool library mentioned in the next section.

use@testing-library/react-hooks

Test hooks with return values

Regarding this tool library, the problems it needs to solve and implementation principles are explained in its code repository. Those who are interested can even look at its source code directly, which is very simple. Here is an example to demonstrate how to test the situation mentioned in the last section, for example, we have a hook:

function useCounter() {
 const [count, setCount] = useState(0)
 const inc = useCallback(() => setCount(c => c + 1), [])
 const dec = useCallback(() => setCount(c => c - 1), [])
 
 return {
  count,
  inc,
  dec
 }
}

First of all, we can test it through the previous section, and we only need to implement a temporary Wrapper, such as:

const CounterIncWrapper = () => {
 const {count, inc} = useCounter()
 return <button onClick={inc}>{count}</button>
}

const CounterDecWrapper = () => {
 const {count, dec} = useCounter()
 return <button onClick={dec}>{count}</button>
}

Then test CounterIncWrapper or CounterDecWrapper separately according to the method mentioned in the previous section, but we will find that the logic of Wrapper here is very similar. Can we extract it into a common logic? The answer is of course OK, which is exactly what @testing-library/react-hooks does, and using it we can test hooks like this, as follows:

test('should increment counter', () => {
 const { result } = renderHook(() => useCounter())

 act(() => {
  ()
 })

 expect().toBe(1)
 
 act(() => {
  ()
 })

 expect().toBe(0)
})

The act here is a built-in tool method. You can refer to the official documentation to understand that any state modification should be performed in its callback function, otherwise an error warning will occur.

Test hooks with dependencies

In some cases, our hook will depend on it. The most common one is the useContext hook, which relies on a Provider parent component, such as the lightweight state management library unstated-next. Suppose we abstract the hook above into an independent Container (the unstated-next API will be involved here, but it does not affect understanding):

const Counter = createContainer(useCounter)

To use this Container we need this:

It can be found that the CounterDisplay here depends on . To test CounterDisplay, we inject the parent component through the wrapper parameter of renderHook, such as:

function CounterDisplay() {
 let counter = ()
 return (
  <div>
   <button onClick={}>-</button>
   <span>{}</span>
   <button onClick={}>+</button>
  </div>
 )
}

function App() {
 return (
  <>
   <CounterDisplay />
  </>
 )
}

In addition, renderHook also supports initialProps parameter, which represents the parameters in the callback function, which will not be described here.

Test side effects

The more difficult thing in hooks should be considered useEffect. I spent a long time looking at how others unit tested it, but I didn't get any useful information. Later, I thought about it carefully and actually thought this question should be thought of this way. useEffect is used to encapsulate side effects. It is only used to be responsible for the running time of side effects. What does the side effects do, and it is completely transparent to useEffect. Therefore, there is no need to unit test it, but we should ensure its correctness at the implementation layer of side effects. But we usually couple the implementation of side effects with the implementation of hooks, so how do we test the implementation of side effects? There are two situations here.

useEffect will run the callback function passed in props

This situation is relatively simple. You only need to construct a spy function through () and then render the hook in the previous section, and verify it by jest's API of the spy function.

useEffect Integrate

In this case, I currently test by directly declaring the side effect code outside the hook, for example:

export function updateDocumentTitle(title) {
  = title
 
 return () => {
    = 'default title'
 }
}

export function useDocumentTitle(title) {
 useEffect(() => updateDocumentTitle(title), [title])
}

In this way, you only need to test the updateDocumentTitle separately, without spending time on useEffect.

Some people may ask here that you cannot override whether the effect is re-run when the title is changed. Indeed, I have no way to solve this problem at present. If you want to solve it, there is still a way to pass updateDocumentTitle through the parameters of useDocumentTitle, but this is very invasive to the code. I do not recommend this. If the implementation method of hook itself is like this, you can write related test cases for it. If not, there is no need to rewrite the original implementation in order to write test cases.

The reason why hook cannot be tested

When writing unit tests on various hooks in company projects, I found that some hooks are very difficult to test, and the general characteristics are as follows:

  • The implementation of hooks is very complex, with many states and many dependencies.
  • The implementation of hook is not complicated, but external dependencies are difficult to mock
  • The hook is self-contained and has no entry

Regarding the first point, the solution is of course to simplify the complex and divide the complex hook into multiple simple hooks, making their responsibilities more single. For the second point, if external dependencies are difficult to mock , I recommend putting its test cases into the integration testing phase for implementation, rather than spending too much effort on the mock logic of writing unit tests. For details on the last solution, please refer to the previous section.

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.