使用 Axios 获取数据的自定义 React Hook 测试
自定义钩子
测试自定义钩子
测试更多用例
结论
Hooks 是 React 的一个新概念。它需要重新思考现有的知识。此外,使用 Hooks 开发 React 组件需要转变思维(例如,不要只考虑生命周期方法)。虽然需要一些时间才能适应,但经过一些练习,Hooks 可以毫无问题地融入到实际项目中。自定义 Hooks 非常有助于将逻辑封装到易于复用的独立模块中。
然而,测试 hooks(目前)并非易事。我花了不少时间为我的自定义 hooks 编写测试用例。这篇文章描述了测试 hooks 的关键环节。
您可以在我的代码沙盒中找到自定义钩子的代码以及相应的测试。
自定义钩子
本文旨在帮助您了解如何编写自定义 React Hooks。如果您是新手,请查看React 文档。另一个不错的起点是查看awesome-react-hooks。
以下代码片段构成一个简单的自定义钩子,用于使用axios执行GET请求。
// useFetch.js
import { useState, useEffect } from "react";
import axios from "axios";
// custom hook for performing GET request
const useFetch = (url, initialValue) => {
const [data, setData] = useState(initialValue);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async function() {
try {
setLoading(true);
const response = await axios.get(url);
if (response.status === 200) {
setData(response.data);
}
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { loading, data };
};
export default useFetch;
以下代码显示了如何使用这个自定义钩子。
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import useFetch from "./useFetch";
function App() {
const { loading, data } = useFetch(
"https://jsonplaceholder.typicode.com/posts/"
);
return (
<div className="App">
{loading && <div className="loader" />}
{data &&
data.length > 0 &&
data.map(blog => <p key={blog.id}>{blog.title}</p>)}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
测试自定义钩子
在撰写本文时,测试 Hooks 并非易事。React 官方文档仅提供了一小部分内容来探讨这个问题。由于违反了Hooks 的规则,我在测试过程中遇到了不少困难。
然而,我发现react-hooks-testing-library可以处理函数组件主体内的运行钩子,并提供各种有用的实用函数。
在编写测试之前,您需要按照文档中的说明安装库及其对等依赖项:
$ npm i -D @testing-library/react-hooks
$ npm i react@^16.8.0
$ npm i -D react-test-renderer@^16.8.0
自定义钩子利用axios来获取数据。我们需要一种方法来模拟实际的网络情况。有很多方法可以做到这一点。我喜欢axios-mock-adapter,它可以轻松编写成功和失败请求的测试。您还需要安装这些库。
$ npm i axios
$ npm i -D axios-mock-adapter
在我们讨论关键部分之前,首先请看一下下面的Jest测试。
// useFetch.test.js
import { renderHook } from "@testing-library/react-hooks";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import useFetch from "./useFetch";
test("useFetch performs GET request", async () => {
const initialValue = [];
const mock = new MockAdapter(axios);
const mockData = "response";
const url = "http://mock";
mock.onGet(url).reply(200, mockData);
const { result, waitForNextUpdate } = renderHook(() =>
useFetch(url, initialValue)
);
expect(result.current.data).toEqual([]);
expect(result.current.loading).toBeTruthy();
await waitForNextUpdate();
expect(result.current.data).toEqual("response");
expect(result.current.loading).toBeFalsy();
});
useFetch的实现使用axios执行网络请求。因此,我们在调用useFetch之前模拟了GET请求。
// ...
const mock = new MockAdapter(axios);
// ...
/*
Mock network call. Instruct axios-mock-adapter
to return with expected data and status code of 200.
*/
mock.onGet(url).reply(200, mockData);
// invoke our custom hook
const { result, waitForNextUpdate } = renderHook(() =>
useFetch(url, initialValue)
);
如你所见,useFetch被包装在renderHook函数调用中。这实际上是为了提供正确的上下文来执行自定义钩子,而不会违反钩子的规则(在这种情况下,钩子只能在函数组件的主体内调用)。
renderHook调用返回一个RenderHookResult 对象。在我们的示例中,我们解构了result 对象,并从中获取了waitForNextUpdate函数。我们先来讨论一下result 对象。
// ...
const { result, waitForNextUpdate } = renderHook(() =>
useFetch(url, initialValue)
);
expect(result.current.data).toEqual([]);
expect(result.current.loading).toBeTruthy();
// ...
result构成了renderHook 的结果。正如您在expect语句中所见,我们可以从result.current访问自定义钩子的实际返回值。因此,result.current.data和result.current.loading保存了自定义钩子调用的返回值。这两个断言的计算结果为 true。data状态保存了传入的初始值,而loading状态为 true,因为实际的网络调用尚未执行。
到目前为止一切顺利,但是我们如何执行调用呢?因此,我们需要waitForNextUpdate。
// ...
const { result, waitForNextUpdate } = renderHook(() =>
useFetch(url, initialValue)
);
expect(result.current.data).toEqual([]);
expect(result.current.loading).toBeTruthy();
await waitForNextUpdate();
expect(result.current.data).toEqual("response");
expect(result.current.loading).toBeFalsy();
waitForNextUpdate允许我们等待异步函数返回以检查网络调用的响应。
以下摘录自该库的文档:
[...] 返回一个 Promise,该 Promise 在下次钩子渲染时解析,通常是在状态由于异步操作而更新时 [...]。
返回后,await waitForNextUpdate()
我们可以放心地断言result.current.data保存着来自(模拟的)网络请求的数据。此外,由于调用setLoading(false)
了该函数,状态发生了改变,因此result.current.loading为false。
测试更多用例
下面的代码片段包含两个额外的测试。第一个测试我们的钩子实现是否可以处理多个调用。第二个测试借助axios-mock-adapter检查网络错误情况。
test("useFetch performs multiple GET requests for different URLs", async () => {
// fetch 1
const initialValue = "initial value";
const mock = new MockAdapter(axios);
const mockData = 1;
const url = "http://mock";
mock.onGet(url).reply(200, mockData);
const { result, waitForNextUpdate } = renderHook(() =>
useFetch(url, initialValue)
);
expect(result.current.data).toEqual("initial value");
expect(result.current.loading).toBeTruthy();
await waitForNextUpdate();
expect(result.current.data).toEqual(1);
expect(result.current.loading).toBeFalsy();
// fetch 2
const url2 = "http://mock2";
const mockData2 = 2;
mock.onGet(url2).reply(200, mockData2);
const initialValue2 = "initial value 2";
const { result: result2, waitForNextUpdate: waitForNextUpdate2 } = renderHook(
() => useFetch(url2, initialValue2)
);
expect(result2.current.data).toEqual("initial value 2");
expect(result2.current.loading).toBeTruthy();
await waitForNextUpdate2();
expect(result2.current.data).toEqual(2);
expect(result2.current.loading).toBeFalsy();
});
test("useFetch sets loading to false and
returns inital value on network error", async () => {
const mock = new MockAdapter(axios);
const initialValue = [];
const url = "http://mock";
mock.onGet(url).networkError();
const { result, waitForNextUpdate } = renderHook(() =>
useFetch(url, initialValue)
);
expect(result.current.data).toEqual([]);
expect(result.current.loading).toBeTruthy();
await waitForNextUpdate();
expect(result.current.loading).toBeFalsy();
expect(result.current.data).toEqual([]);
});
结论
我非常喜欢react-hooks-testing-library的 API 。但最让我喜欢的是,这个库让我能够直接测试自定义 hooks。个人认为,用这个库测试起来非常简单。
如果您在控制台中看到如以下屏幕截图所示的恼人的警告,则很有可能通过更新依赖项来修复它。
该行为警告已通过 react@^16.9.0 和 @testing-library/react-hooks@^2.0.0 版本得到解决。
鏂囩珷鏉ユ簮锛�https://dev.to/doppelmutzi/testing-of-a-custom-react-hook-for-fetching-data-with-axios-4gf1