如何使用异步函数和 setState 测试 useEffect
我已经建立了一个 github 项目,以了解如何更好地测试 react (v 16.8.0) useEffect 钩子。 我进行 api 调用以获取 useEffect 中的数据,并将接收到的数据设置为状态组件元素。 我的组件将查询作为 prop 接收,如果查询 prop 字符串不为空,则进行 api 调用。我想测试一下,使用非空查询 prop,api 调用是否已完成,组件是否正确设置了其状态。
我知道测试 useEffect 时要面临的问题是,与 useEffect 相关的效果不会阻止浏览器更新屏幕,因此测试在 useEffect 完成工作之前就结束了。 我从 React 文档中了解到, react-test-utils 中有一个名为 act 的 API,它被认为可以包装渲染组件并对其进行更新的代码。 即使我尝试使用它,我的代码仍然会遇到同样的问题。
这是我尝试测试的组件:
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>
);
};
这是我为其编写的测试:
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);
});
我不断收到警告,告诉我
An update to DisplayData inside a test was not wrapped in act(...)
我的测试失败,因为 liCounts 收到的是 is_0_,而不是预期的 2 。
插入相同的控制台消息来调试应用程序,我相信问题在于 useEffect 在测试执行后启动,但我不知道如何继续。
更新 感谢@jonrsharpe,我使用具有异步版本的 React 版本 16.9.0-alpha.0 解决了我的问题 act api。
这是单元测试解决方案:
我们使用
jest.spyOn(axios, 'get')
来模拟
axios.get
方法及其解析/拒绝的值,而无需触及真实网络。这样我们的单元测试就可以运行在一个没有副作用的环境中,并且与系统环境、网络环境等隔离。
我们使用 act() 助手来确保获取的数据已经渲染并且 UI 已经更新。
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:
最后,我们断言
axios.get
方法是否被调用,并通过快照测试,断言
data
是否被正确渲染
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();
});
});
单元测试结果 100%覆盖率:
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>
`;
依赖项版本:
"jest": "^24.9.0",
"react-test-renderer": "^16.11.0",
"react": "^16.11.0",
"axios": "^0.19.0",
源代码: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/56410688