测试使用 Hooks 获取数据的 React 组件
Posted
技术标签:
【中文标题】测试使用 Hooks 获取数据的 React 组件【英文标题】:Testing React components that fetches data using Hooks 【发布时间】:2019-07-29 13:27:16 【问题描述】:我的 React 应用程序有一个从远程服务器获取数据以显示的组件。在 pre-hooks 时代,componentDidMount()
是个好去处。但现在我想为此使用钩子。
const App = () =>
const [ state, setState ] = useState(0);
useEffect(() =>
fetchData().then(setState);
);
return (
<div>... data display ...</div>
);
;
我使用 Jest 和 Enzyme 的测试如下所示:
import React from 'react';
import mount from 'enzyme';
import App from './App';
import act from 'react-test-renderer';
jest.mock('./api');
import fetchData from './api';
describe('<App />', () =>
it('renders without crashing', (done) =>
fetchData.mockImplementation(() =>
return Promise.resolve(42);
);
act(() => mount(<App />));
setTimeout(() =>
// expectations here
done();
, 500);
);
);
测试成功,但记录了一些警告:
console.error node_modules/react-dom/cjs/react-dom.development.js:506
Warning: An update to App inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() =>
/* fire events that update state */
);
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at (redacted)
in App (created by WrapperComponent)
in WrapperComponent
App 组件的唯一更新发生在 Promise 回调中。我如何确保这发生在 在 act
块内?文档明确建议在act
块外部进行断言。此外,将它们放在里面不会改变警告。
【问题讨论】:
此代码将调用fetchData
两次,如果fetchData
返回不同的数据则进入无限循环。您应该将[]
作为第二个参数传递给useEffect
以模拟componentDidMount
。否则每次渲染都会调用useEffect
。首先fetchData
导致重新渲染。每当setState
获得新值时,它都会导致额外的渲染。
github.com/kentcdodds/react-testing-library/issues/… 似乎正在讨论这种情况
我不确定,但github.com/threepointone/react-act-examples 看起来很有希望
感谢@UjinT34 的评论。事实上,我有 []
作为部门,但认为它与这个特定问题无关。事实上,它应该在那里防止过于频繁地调用fetchData
。尽管如此,关于不使用act
的警告仍然存在:(
感谢@skyboyer 的建议。您提到的回购的重点是他们使用“手动”模拟,他们可以手动解决 - 在 act
语句中。我更喜欢使用 Jests 模拟可能性。我的感觉是,从 Jest 模拟中解决 Promise 发生在 act
之外,从而触发了警告。但我不知道如何解决。
【参考方案1】:
我遇到了同样的问题,最后编写了一个库,通过模拟所有标准 React Hooks 来解决这个问题。
基本上,act()
是一个同步函数,就像useEffect
,但useEffect
执行一个异步函数。 act() 不可能“等待”它执行。开火即忘!
文章在这里:https://medium.com/@jantoine/another-take-on-testing-custom-react-hooks-4461458935d4
这里的库:https://github.com/antoinejaussoin/jooks
要测试您的代码,您首先需要将您的逻辑(获取等)提取到一个单独的自定义挂钩中:类似于:
const useFetchData = () =>
const [ state, setState ] = useState(0);
useEffect(() =>
fetchData().then(setState);
);
return state;
然后,使用 Jooks,您的测试将如下所示:
import init from 'jooks';
[...]
describe('Testing my hook', () =>
const jooks = init(() => useFetchData());
// Mock your API call here, by returning 'some mocked value';
it('Should first return 0', () =>
const data = jooks.run();
expect(data).toBe(0);
);
it('Then should fetch the data and return it', async () =>
await jooks.mount(); // Fire useEffect etc.
const data = jooks.run();
expect(data).toBe('some mocked value');
);
);
【讨论】:
【参考方案2】:Enzyme 不支持挂钩,因为它是一个相对较新的功能: https://github.com/airbnb/enzyme/issues/2011
也许你可以同时使用简单的 Jest? 也不要担心警告,它应该会在 React 16.9.0 发布时消失(请参阅此拉取请求 https://github.com/facebook/react/pull/14853)
【讨论】:
请注意,后半部分提到的 PR 描述了act
的异步版本,您必须采用它才能使消息“消失”。简单的玩笑也不会解决它。
@erich2k8 你是对的。无论如何,当我升级到 React 16.9.0 时,警告消失了。【参考方案3】:
我已经创建了测试异步钩子的示例。
https://github.com/oshri6688/react-async-hooks-testing
CommentWithHooks.js
:
import getData from "services/dataService";
const CommentWithHooks = () =>
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const fetchData = () =>
setIsLoading(true);
getData()
.then(data =>
setData(data);
)
.catch(err =>
setData("No Data");
)
.finally(() =>
setIsLoading(false);
);
;
useEffect(() =>
fetchData();
, []);
return (
<div>
isLoading ? (
<span data-test-id="loading">Loading...</span>
) : (
<span data-test-id="data">data</span>
)
<button
style= marginLeft: "20px"
data-test-id="btn-refetch"
onClick=fetchData
>
refetch data
</button>
</div>
);
;
CommentWithHooks.test.js
:
import React from "react";
import mount from "enzyme";
import act from "react-dom/test-utils";
import MockPromise from "testUtils/MockPromise";
import CommentWithHooks from "./CommentWithHooks";
import getData from "services/dataService";
jest.mock("services/dataService", () => (
getData: jest.fn(),
));
let getDataPromise;
getData.mockImplementation(() =>
getDataPromise = new MockPromise();
return getDataPromise;
);
describe("CommentWithHooks", () =>
beforeEach(() =>
jest.clearAllMocks();
);
it("when fetching data successed", async () =>
const wrapper = mount(<CommentWithHooks />);
const button = wrapper.find('[data-test-id="btn-refetch"]');
let loadingNode = wrapper.find('[data-test-id="loading"]');
let dataNode = wrapper.find('[data-test-id="data"]');
const data = "test Data";
expect(loadingNode).toHaveLength(1);
expect(loadingNode.text()).toBe("Loading...");
expect(dataNode).toHaveLength(0);
expect(button).toHaveLength(1);
expect(button.prop("onClick")).toBeInstanceOf(Function);
await getDataPromise.resolve(data);
wrapper.update();
loadingNode = wrapper.find('[data-test-id="loading"]');
dataNode = wrapper.find('[data-test-id="data"]');
expect(loadingNode).toHaveLength(0);
expect(dataNode).toHaveLength(1);
expect(dataNode.text()).toBe(data);
);
testUtils/MockPromise.js
:
import act from "react-dom/test-utils";
const createMockCallback = callback => (...args) =>
let result;
if (!callback)
return;
act(() =>
result = callback(...args);
);
return result;
;
export default class MockPromise
constructor()
this.promise = new Promise((resolve, reject) =>
this.promiseResolve = resolve;
this.promiseReject = reject;
);
resolve(...args)
this.promiseResolve(...args);
return this;
reject(...args)
this.promiseReject(...args);
return this;
then(...callbacks)
const mockCallbacks = callbacks.map(callback =>
createMockCallback(callback)
);
this.promise = this.promise.then(...mockCallbacks);
return this;
catch(callback)
const mockCallback = createMockCallback(callback);
this.promise = this.promise.catch(mockCallback);
return this;
finally(callback)
const mockCallback = createMockCallback(callback);
this.promise = this.promise.finally(mockCallback);
return this;
【讨论】:
【参考方案4】:我使用以下步骤解决了这个问题
-
将 react 和 react-dom 更新到 16.9.0 版本。
安装再生器运行时
在安装文件中导入 regenerator-runtime。
import "regenerator-runtime/runtime";
import configure from "enzyme";
import Adapter from "enzyme-adapter-react-16";
configure(
adapter: new Adapter()
);
包装挂载和其他可能导致动作内部状态变化的动作。从简单的 react-dom/test-utils、async 和 await 导入 act,如下所示。
import React from 'react';
import mount from 'enzyme';
import App from './App';
import act from "react-dom/test-utils";
jest.mock('./api');
import fetchData from './api';
describe('<App />', () =>
it('renders without crashing', async (done) =>
fetchData.mockImplementation(() =>
return Promise.resolve(42);
);
await act(() => mount(<App />));
setTimeout(() =>
// expectations here
done();
, 500);
);
);
希望这会有所帮助。
【讨论】:
【参考方案5】:这个问题是由组件内部的许多更新引起的。
我遇到了同样的问题,这将解决问题。
await act( async () => mount(<App />));
【讨论】:
我收到Warning: Do not await the result of calling TestRenderer.act(...), it is not a Promise.
以上是关于测试使用 Hooks 获取数据的 React 组件的主要内容,如果未能解决你的问题,请参考以下文章
React Hooks ,函数 fetchData 不是反应组件?
如何在 React hooks 和 redux 中只获取一次且不再获取数据