使

使用 Axios 获取数据的自定义 React Hook 测试自定义 Hook 测试自定义 Hook 测试更多用例结论

2025-06-08

使用 Axios 获取数据的自定义 React Hook 测试

自定义钩子

测试自定义钩子

测试更多用例

结论

Hooks 是 React 的一个新概念。它需要重新思考现有的知识。此外,使用 Hooks 开发 React 组件需要转变思维(例如,不要只考虑生命周期方法)。虽然需要一些时间才能适应,但经过一些练习,Hooks 可以毫无问题地融入到实际项目中。自定义 Hooks 非常有助于将逻辑封装到易于复用的独立模块中。

然而,测试 hooks(目前)并非易事。我花了不少时间为我的自定义 hooks 编写测试用例。这篇文章描述了测试 hooks 的关键环节。

Jooks 是一个库,指出 React 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;
Enter fullscreen mode Exit fullscreen mode

以下代码显示了如何使用这个自定义钩子。

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);
Enter fullscreen mode Exit fullscreen mode

测试自定义钩子

在撰写本文时,测试 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
Enter fullscreen mode Exit fullscreen mode

自定义钩子利用axios来获取数据。我们需要一种方法来模拟实际的网络情况。有很多方法可以做到这一点。我喜欢axios-mock-adapter,它可以轻松编写成功和失败请求的测试。您还需要安装这些库。

$ npm i axios
$ npm i -D axios-mock-adapter
Enter fullscreen mode Exit fullscreen mode

在我们讨论关键部分之前,首先请看一下下面的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();
});
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

如你所见,useFetch被包装在renderHook函数调用中。这实际上是为了提供正确的上下文来执行自定义钩子,而不会违反钩子的规则(在这种情况下,钩子只能在函数组件的主体内调用)。

renderHook调用返回一个RenderHookResult 对象。在我们的示例中,我们解构了result 对象,并从中获取waitForNextUpdate函数。我们先来讨论一下result 对象

// ...
const { result, waitForNextUpdate } = renderHook(() =>
  useFetch(url, initialValue)
);

expect(result.current.data).toEqual([]);
expect(result.current.loading).toBeTruthy();
// ...
Enter fullscreen mode Exit fullscreen mode

result构成了renderHook 的结果。正如您在expect语句中所见,我们可以从result.current访问自定义钩子的实际返回值。因此,result.current.dataresult.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();
Enter fullscreen mode Exit fullscreen mode

waitForNextUpdate允许我们等待异步函数返回以检查网络调用的响应。

以下摘录自该库的文档

[...] 返回一个 Promise,该 Promise 在下次钩子渲染时解析,通常是在状态由于异步操作而更新时 [...]。

返回后,await waitForNextUpdate()我们可以放心地断言result.current.data保存着来自(模拟的)网络请求的数据。此外,由于调用setLoading(false)了该函数,状态发生了改变,因此result.current.loadingfalse

测试更多用例

下面的代码片段包含两个额外的测试。第一个测试我们的钩子实现是否可以处理多个调用。第二个测试借助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([]);
});
Enter fullscreen mode Exit fullscreen mode

结论

我非常喜欢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
PREV
升级 macOS 以利用键盘快捷键实现更高效的开发工作流程 Alfred – 强大的应用程序启动器 Spectacle – 缺失的窗口调整器 Amethyst – 便捷的平铺窗口管理器 Contexts – 改进的 Command-Tab 切换器 macOS 内置快捷键 KeyCue – 快捷键一目了然 摘要
NEXT
5 Things No One Tells You About Going to a Coding BootCamp Deer in headlights: Outside Resources: Community How Many rounds? Career Prep