If your event function has been wrapped by a setTimeOut function like debounce/throttle, then we need to use async/await and waitFor(() => {}) to help us.
import { render, act, waitFor } from"@testing-library/react";import Component from"./index";describe('Component', () => {// usually we will use debounce to wrap a change functionit('should do something when trigger some event, async () => { const spy =vi.fn(); const { queryByTestId } =render( <ComponentonChangeWrappedByDebounce={spy} /> ); const inputEle =queryByTestId('input');act(() => {fireEvent.change(inputEle, {target: {value:'23'}}); });// due to use the debounceawaitwaitFor(() =>expect(spy).toHaveBeenCalledTimes(1)); });});
Test hook function
Usually, we will write hooks to help us, so we need to test the hook function too, for hook we need to use renderHook() to help us.
renderHook also is provided by @testing-library/react.
Here is a hook function that I write to get detail data, I use the useEffect to update the detail when I change the dependencies, and I use a loading status from the redux store.
First I write a function to mock a fetch function, because we use the Promise the setTtimeOut function, so we need to use async/await.
As I noticed I use a loading status from redux store, so we also need to mock the redux, so I write the ReduxProviderWrapper.
All right, all the setups are ready, now we use the renderHook() to render our hook, the first param of the renderHook is a function that we can set the props, the props are what the hook function params have been, and we can set initialProps in the second param of the renderHook, so that we can use the rerender(newProps) returned by renderHook to change dependencies, and test the result if is correct, and don't forget to set the wrapper ReduxProviderWrapper in the second param of renderHook when we use the redux.
Now we finished most tests of this hook, but if we need to get our coverage to be 100% we still need to test one if we don't set any dependency, we just need to add one more test case.
it('should return the null without any dependency',async () => {const { result } =renderHook( () =>useDetailData(getMockPromise('data'), []), { wrapper: ReduxProviderWrapper } );awaitwaitFor(() => {expect(result.current).toBe(null); });});
Then we can get our satisfactory test coverage report.
Test coverage
Test with react-router
If we use the react-router, we also need to provide the router context when we test it.
How to provide router context? Using MemoryRouter which is provided by react-router.
For example, I test a hook using the router hook useLocation(), so I need to provide a router context as a wrapper for renderHook().
Through the initialEntries props to mock the route currently used.
import { render } from"@testing-library/react";import useQuery from"./useQuery";import { RouterProviderWrapper } from"../../test/utils";constRouterProviderWrapper= (props:MemoryRouterProps) => ( <MemoryRouter {..._omit(props,'children')}> {props.children} </MemoryRouter>);describe("useQuery", () => {it('should return the query when there is a query string on the url.', () => {constTest= () => {constquery=useQuery().get('value');return <divdata-testid="test">{query}</div>; }const { queryByTestId } =render( <RouterProviderWrapperinitialEntries={['/test-url?value=testValue']}> <Test /> </RouterProviderWrapper> );constele=queryByTestId('test');expect(ele).toHaveTextContent('testValue'); });})
Test with the mock function
Sometimes we should mock some functions to return the result as we need so that we can test the function's behavior if is correct.
For this vitest provide the vi.mock('path/of/import/function') & vi.mocked().mockReturnValue().
usePosts.ts
In this hook, the request function getPost is a fetch function imported from the request file, but when we test it we don't want to get the real data back, we need to use mock data so that we can control the expct statement.
At the end of this document, we have learned so many skills of writing unit tests for react, I hope those skills can help you to write unit tests easier.