I have set up a github project to understand how to better test react (v 16.8.0) useEffect hook. I make an api call to fetch data inside useEffect and I set the received data as state component element. My component receives the query as a prop and make the api call if the query prop string is not empty. I would like to test that with a no-empty query prop the api call is made and the component set its state right.
I know that the problem to be faced testing useEffect is that the effects related to useEffect don't block the browser from updating the screen so the tests come to their end before the useEffect makes its job. I read from React documentation that there is an API from react-test-utils called act that is thought to wrap the code rendering the component and performing updates on it. Even if I tried to use it I keep on having the same problems with my code.
This is the component I am trying to test:
const DisplayData = ({ query, onQueryChange }) => {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
};
if (!!query) fetchData();
}, [query]);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
};
and this is the test I wrote for it:
it("should show new entries when query is set", () => {
const el = document.createElement("div");
document.body.appendChild(el);
axios.get.mockResolvedValue({ data: { hits: FAKE_HITS } });
act(() => {
render(<DisplayData query='pippo' />, el);
});
const liCounts = el.querySelectorAll("li");
expect(liCounts.length).toBe(2);
});
I keep on receiving a warning telling me that
An update to DisplayData inside a test was not wrapped in act(...)
and my test failed because the liCounts received is_0_ instead of the expected 2.
Inserting same console messages to debug the application, I trust that the problem is that useEffect is launched after the test execution but I don't know any more how to proceed.
UPDATE Thanks to @jonrsharpe I solved my issue using React version 16.9.0-alpha.0 that has an async version of act api.
Here is the unit test solution:
We use jest.spyOn(axios, 'get')
to mock axios.get
method and its resolved/rejected value without hitting the real network. This allows our unit tests to run in an environment that has no side effects and is isolated from the system environment, network environment, etc.
We use act() helper to make sure the fetched data rendered and UI has been updated.
When writing UI tests, tasks like rendering, user events, or data fetching can be considered as “units” of interaction with a user interface. react-dom/test-utils provides a helper called act() that makes sure all updates related to these “units” have been processed and applied to the DOM before you make any assertions:
In the end, We assert whether the axios.get
method is called, and through snapshot testing, assert whether data
is rendered correctly
index.tsx
:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
export const DisplayData = ({ query, onQueryChange }) => {
const [data, setData] = useState<any>({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(`http://hn.algolia.com/api/v1/search?query=${query}`);
setData(result.data);
};
if (!!query) fetchData();
}, [query]);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
};
index.spec.tsx
:
import React from 'react';
import { DisplayData } from './';
import axios from 'axios';
import renderer, { act } from 'react-test-renderer';
describe('DisplayData', () => {
it('should show new entries when query is set', async () => {
const mProps = {
query: 'pippo',
onQueryChange: jest.fn()
};
const FAKE_HITS = [{ objectID: 1, url: 'haha.com', title: 'haha' }];
const axiosGetSpy = jest.spyOn(axios, 'get').mockResolvedValueOnce({ data: { hits: FAKE_HITS } });
let component;
await act(async () => {
component = renderer.create(<DisplayData {...mProps}></DisplayData>);
});
expect(axiosGetSpy).toBeCalledWith('http://hn.algolia.com/api/v1/search?query=pippo');
expect(component.toJSON()).toMatchSnapshot();
axiosGetSpy.mockRestore();
});
it('should not fetch data when query is empty string', async () => {
const mProps = {
query: '',
onQueryChange: jest.fn()
};
const axiosGetSpy = jest.spyOn(axios, 'get');
let component;
await act(async () => {
component = renderer.create(<DisplayData {...mProps}></DisplayData>);
});
expect(axiosGetSpy).not.toBeCalled();
expect(component.toJSON()).toMatchSnapshot();
axiosGetSpy.mockRestore();
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/56410688/index.spec.tsx
DisplayData
✓ should show new entries when query is set (28ms)
✓ should not fetch data when query is empty string (5ms)
-----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.tsx | 100 | 100 | 100 | 100 | |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 2 passed, 2 total
Time: 3.666s
index.spec.tsx.snap
:
// Jest Snapshot v1,
exports[`DisplayData should not fetch data when query is empty string 1`] = `<ul />`;
exports[`DisplayData should show new entries when query is set 1`] = `
<ul>
<li>
<a
href="haha.com"
>
haha
</a>
</li>
</ul>
`;
Dependencies versions:
"jest": "^24.9.0",
"react-test-renderer": "^16.11.0",
"react": "^16.11.0",
"axios": "^0.19.0",
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/56410688