How to test Custom React Hooks?
Tue, Mar 8, 2022 •8 min read
Category: Code Stories / Software development
According to Stackoverflow 2021 survey results, React is the most popular web framework amongst front-end developers worldwide. In a nutshell, React is a component-based JavaScript library used to build user interfaces. Today we’ll learn how to test React Hooks, which were first introduced in 2018 and since then have been helping engineers build better apps.
What are React Hooks?
Let’s start with the basics. Hooks were first released in React 16.8 in October of 2018 to enable developers to use state and other React features without writing a class or a HOC. One of their biggest benefits is that they work with the existing code and therefore can be easily adopted into your codebase. Hooks are an opt-in tool, but over the years have proven to continuously make devs' lives easier, quickly becoming one of the most popular React features.
There are 10 built-in hooks that were released with React 16.8, but the most commonly used ones are:
State Hook – lets devs manage state inside functional components without the need of converting it to a class component. Additionally, the State Hook receives an initial state as an argument and adds a second function which allows developers to update it.
const [count, setCount = useState(0)
Effect Hook – enables React developers to perform side-effects in the function components. It’s equivalent to componentDidMount, componentDidUpdate, and componentWillUnmount in class components, but unified into a single API.
useEffect(() => {
const interval = setInterval(() => {
checkUserStatus()
}, 1000 * 60 * 5)
return () => clearInterval(interval)
}, [])
Custom Hook – a custom hook is a JavaScript function which uses other hooks inside of it and contains a common stateful logic that can be reused within multiple components. It minifies your codebase making it less repetitive. In this post we focus on this type of hook and how to test it.
const useFetch = (url) => {
const [data, setData] = useState(null)
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => setData(data))
}, [url])
return [data]
}
What to use for testing custom hooks?
After carefully crafting your custom hook you probably wonder “will it work over the lifetime of the app I’m building?”. The best way of finding out is to test the hook, preferably with Jest and/or React Hooks Testing Library.
Jest is a JS testing framework. It's an easy way to test your code that also helps mocking functions and any other objects. Additionally, when a test fails for some reason, it provides a rich context why that happened
React Hooks Testing Library is a set of utilities that, as the name suggests, lets you test your custom hooks. You don’t have to create components to test your hook, nor to figure out how to trigger all the cases in which it could be updated. It’s a hassle-free way of testing even more advanced and complex hooks.
Jest lets you access the DOM and mocking functions so you have more control over how the code executes, while React Testing Library provides tools to test and render hooks without getting into implementation details. It’s the combination of both Jest and React Testing Library that will give you the best results while testing.
Now that we have all the basics covered, let us guide you through testing one of our own custom hooks.
1st case: unit testing simple custom hooks
The hook we’ve created is called useTextField and it contains its own state and function to change it. This hook can be used to handle value as local state and behavior for any input. Here’s how it looks:
export const useTextField = () => {
const [value, setValue] = useState(‘’)
const onChange = (event) => {
setValue(event.target.value)
}
return {
value,
onChange
}
}
First, we need to create a test using describe. Then, we render a hook to use it for testing. Common way of doing it is to use the renderHook method from the React Hooks Testing Library. The next step is to figure out which cases we need to check. The best way to take off with testing is to start with two cases - in the first one we won’t implement any changes to the value, in the second we’ll update the value. Additionally, it's a good practice to test the edge case as well - to see how our hook will behave under extreme operating parameters.
There are three scenarios in our custom hook that we should test:
1. If the hook renders properly and has exact default values we intended it to have
describe(‘useTextField hook’, () => {
it(‘checking default state’, () => {
const { result } = renderHook(() => useTextField())
expect(result.current.value).toBe(‘’)
expect(result.current.onChange).toBeInstanceOf(Function)
})
})
2. If onChange function works and changes state properly
Here we need to use the act() method from React Hooks Testing Library. This makes your test run closer to how React works in the browser.
it(‘onChange - check new value’, () => {
const { result } = renderHook(() => useTextField())
expect(result.current.value).toBe(‘’)
act(() => {
result.current.onChange({target: {value: ‘new value’}})
})
expect(result.current.value).toBe(‘new value’)
})
3. Finally, let’s test If after unmount and rerender hook, value is back to default
it(‘check if after unmount and rerender hook, value is default’, () => {
const { result, unmount, rerender } = renderHook(() => useTextField())
expect(result.current.value).toBe(‘’)
act(() => {
result.current.onChange({target: {value: ‘new value’}})
})
expect(result.current.value).toBe(‘new value’)
unmount()
rerender(true)
expect(result.current.value).toBe(‘’)
})
That’s it! After testing those three variables we are safe to use the custom hook in our app.
2nd case: testing more advanced custom hooks
Since we now see how it works in a simplified example, let’s try something more advanced. Here is a more complicated hook we have created. useCountDown is a timer that uses its own state, timeout, and interval functions.
The best way to test timers like this one is to use useFakeTimers provided by Jest and only things that need to be tested are the results at different intervals. We have added two functions which are beforeEach and afterEach. In the first one, we want to mock our timer functions used in this hook; however after each test we want to restore their normal behavior using jest.useRealTimers(). Now that we have a test environment set up, let's test our hook.
https://github.com/Bartek-str/timer-tests/blob/master/src/hooks/useCountDown.test.ts
After rendering useCountDown with 3 seconds props, timeLeft should be set to 3 seconds and isFinished should be false. We use act() with jest.advanceTimersByTime() to move time forward in the tests. This function shifts the time by the specified value in milliseconds. All done!
It’s a wrap!
Testing custom hooks can be very difficult depending on the complexity of the hook. Best way to do it is to mock some functions or API calls using Jest. However, don’t forget to make custom hooks simple and reusable so they are easy to implement in a project. Then testing them will become much easier. Good luck with your custom hooks testing!