新闻中心

如何测试包含多个 useQuery 的 React 自定义 Hook

2025-10-07
浏览次数:
返回列表

如何测试包含多个 usequery 的 react 自定义 hook

本文详细阐述了如何使用 React Testing Library 和 React Query 有效测试包含多个 useQuery 操作的自定义 Hook。核心内容包括:采用 jest.mock 对 API 模块进行全局模拟,确保每个测试用例的隔离性;将相关断言合并到单个测试中以提高效率;以及理解 useQuery 返回值 的正确模拟方式,从而避免测试中出现 undefined 错误,确保测试的准确性和健壮性。

引言

在 React 应用开发中,自定义 Hook 是封装可复用逻辑的强大工具,尤其当它们涉及到数据获取时,react-query (或 TanStack Query) 常常是首选。然而,当一个自定义 Hook 内部包含多个 useQuery 调用以获取不同数据时,如何对其进行有效且可靠的测试,常常会遇到挑战。本教程将深入探讨测试此类 Hook 时常见的陷阱,并提供一套健壮的解决方案。

挑战与常见问题

考虑一个自定义 Hook,它通过 react-query 同时获取用户数据和用户状态:

// TestHook.js
import { useQuery } from "react-query";
import { getTestByUid, getTestStatusesByUid } from "./api"; // 假设 API 在单独的文件中

export const useTest = (uid) => {
  const { data: test } = useQuery(["test", uid], () => getTestByUid(uid));
  const { data: testStatuses } = useQuery(["statuses", uid], () => getTestStatusesByUid(uid));

  return {
    test,
    testStatuses,
  };
};

在测试上述 Hook 时,开发者可能遇到以下问题:

  1. 测试隔离性不足: 多个测试用例之间共享模拟(mock)状态,导致前一个测试的模拟影响后一个测试。例如,在一个测试中只模拟了 getTestByUid,而另一个测试依赖于 getTestStatusesByUid,此时未被模拟的 API 调用可能返回 undefined。
  2. 模拟值结构不正确: useQuery Hook 的 data 字段直接包含 API 调用返回的数据。如果 API 模拟返回的是 { data: actualData } 这样的嵌套结构,那么 useQuery 最终得到的 data 将是 { data: actualData } 而非 actualData,导致断言失败。
  3. 冗余的测试用例: 将 Hook 的不同输出分别放置在独立的测试用例中,可能导致重复的设置代码和不必要的复杂性,尤其当这些输出是紧密关联时。

解决方案与最佳实践

为了克服上述挑战,我们将采用以下策略:

1. 彻底的 API 模块模拟

使用 jest.mock() 对整个 API 模块进行模拟,然后在每个测试用例中,利用模拟函数的 mockResolvedValue() 或 mockRejectedValue() 方法,为特定的 API 调用设置预期的返回值。这确保了每个测试用例都拥有一个干净且独立的模拟环境。

// api.js
// 这是一个模拟的 API 模块,实际应用中会包含真实的 API 调用逻辑
export const getTestByUid = (uid) => {
  // 实际的 API 调用
  return Promise.resolve({ id: uid, name: "real test data" });
};

export const getTestStatusesByUid = (uid) => {
  // 实际的 API 调用
  return Promise.resolve(["real_status_1", "real_status_2"]);
};

在测试文件中,我们首先模拟整个 api.js 模块:

// test-hook.test.js
import * as testApi from './api'; // 引入 API 模块

jest.mock('./api'); // 在文件顶部模拟整个 API 模块

2. 确保测试用例的隔离性

在每个 it 或 test 块内部,为所有相关的 API 调用设置其 mockResolvedValue。这样,即使一个 Hook 内部有多个异步操作,每个操作的模拟值都是明确且独立的,不会受到其他测试用例的影响。

察言观数AskTable 察言观数AskTable

企业级AI数据表格智能体平台

察言观数AskTable 78 查看详情 察言观数AskTable

3. 合理组织测试用例

如果一个自定义 Hook 的多个输出是其核心功能的一部分,并且它们在逻辑上是紧密关联的,那么将它们的断言合并到一个测试用例中会更高效和清晰。这减少了重复的 renderHook 调用和 waitForNextUpdate 等待。

4. 正确模拟 useQuery 的返回值

useQuery Hook 的 data 属性直接返回 API Promise 解析后的值。因此,当模拟 API 函数时,mockResolvedValue 应该直接返回期望的数据,而不是一个包含 data 属性的对象。

错误示例: testApi.getTestByUid.mockResolvedValue({ data: { name: 'secret test' } });正确示例: testApi.getTestByUid.mockResolvedValue({ name: 'secret test' });

完整的示例代码

以下是根据上述最佳实践重构后的测试代码:

api.js (模拟的 API 模块)

// src/api/test-api.js
// 实际应用中的 API 调用函数
export const getTestByUid = (uid) => {
  // 假设这里是实际的 axios.get(...) 或 fetch(...) 调用
  return Promise.resolve({ id: uid, name: "default test" });
};

export const getTestStatusesByUid = (uid) => {
  // 假设这里是实际的 axios.get(...) 或 fetch(...) 调用
  return Promise.resolve(["default_status_1", "default_status_2"]);
};

TestHook.js (自定义 Hook)

// src/hooks/TestHook.js
import { useQuery } from "react-query";
import { getTestByUid, getTestStatusesByUid } from "../api/test-api";

export const useTest = (uid) => {
  const { data: test } = useQuery(["test", uid], () => getTestByUid(uid));
  const { data: testStatuses } = useQuery(["statuses", uid], () => getTestStatusesByUid(uid));

  return {
    test,
    testStatuses,
  };
};

test-hook.test.js (测试文件)

// test/test-hook.test.js
import { renderHook } from "@testing-library/react-hooks";
import { QueryClient, QueryClientProvider } from "react-query";
import { useTest } from "../src/hooks/TestHook";
import * as testApi from "../src/api/test-api"; // 引入 API 模块
import React from "react";

// 在文件顶部模拟整个 API 模块
jest.mock("../src/api/test-api");

// 创建一个 QueryClient 实例,并配置默认选项,例如禁用重试
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false, // 在测试中禁用重试,避免不必要的等待
    },
  },
});

// 创建一个包装器组件,用于提供 QueryClientProvider
const wrapper = ({ children }) => {
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

describe("useTestHook", () => {
  it("应该正确返回测试数据和状态", async () => {
    // 为当前测试用例模拟所有相关的 API 调用
    testApi.getTestByUid.mockResolvedValue({ name: "secret test" });
    testApi.getTestStatusesByUid.mockResolvedValue([
      "in_progress",
      "ready_for_approval",
      "rejected",
    ]);

    // 渲染 Hook
    const { result, waitForNextUpdate } = renderHook(
      () => useTest("bb450409-d778-4d57-a4b8-70fcfe2087bd"),
      { wrapper }
    );

    // 等待 Hook 内部的异步操作完成并更新
    await waitForNextUpdate();

    // 断言 Hook 返回的测试数据
    expect(result.current.test).toEqual({ name: "secret test" });
    // 断言 Hook 返回的测试状态
    expect(result.current.testStatuses).toEqual([
      "in_progress",
      "ready_for_approval",
      "rejected",
    ]);
  });

  // 可以添加其他测试用例,例如测试错误状态、加载状态等
  it("应该在 API 调用失败时处理错误", async () => {
    const errorMessage = "Failed to fetch data";
    testApi.getTestByUid.mockRejectedValue(new Error(errorMessage));
    testApi.getTestStatusesByUid.mockResolvedValue([]); // 即使一个失败,另一个也可能成功或被模拟

    const { result, waitForNextUpdate } = renderHook(
      () => useTest("some-uid"),
      { wrapper }
    );

    await waitForNextUpdate();

    // 假设 useQuery 的错误会被 Hook 内部处理或暴露
    // 这里我们只关注 getTestByUid 的错误,testStatuses 可能是默认值或空
    // 实际断言取决于 Hook 如何处理错误
    // expect(result.current.testError).toBeInstanceOf(Error);
    // expect(result.current.testError.message).toBe(errorMessage);
    expect(result.current.test).toBeUndefined(); // 如果 Hook 没有特殊处理,失败的查询数据将是 undefined
    expect(result.current.testStatuses).toEqual([]);
  });
});

注意事项与总结

  1. 全局模拟与局部模拟: jest.mock('./api') 是全局模拟,它替换了整个模块。在每个测试用例中,使用 testApi.getTestByUid.mockResolvedValue(...) 则是对模拟模块中特定函数的行为进行局部配置。这种组合是测试异步 Hook 的强大模式。
  2. QueryClientProvider: 确保你的测试环境包裹在 QueryClientProvider 中,因为 useQuery 依赖于它。
  3. waitForNextUpdate: renderHook 返回的 waitForNextUpdate 是等待 Hook 内部的异步更新完成的关键。对于多个 useQuery 调用,一次 await waitForNextUpdate() 通常足以等待所有初始查询完成,因为 react-query 会在所有依赖项就绪后进行一次渲染。
  4. 断言类型: 对于对象和数组的比较,请使用 toEqual() 而不是 toBe(),因为 toBe() 检查的是引用相等性,而 toEqual() 检查的是值相等性。
  5. 错误处理: 编写测试来验证 Hook 如何处理 API 错误和加载状态是至关重要的。

通过遵循这些原则,你可以有效地测试包含多个 useQuery 的 React 自定义 Hook,确保其功能的健壮性和可靠性。

依赖版本

在撰写本教程时,以下是使用的关键库版本:

  • react-query: ^3.34.7 (或 TanStack Query v3)
  • react: ^16.14.0 (或更高版本,@testing-library/react-hooks 支持 React 16.9+)
  • @testing-library/react-hooks: ^8.0.1

以上就是如何测试包含多个 useQuery 的 React 自定义 Hook的详细内容,更多请关注其它相关文章!


# 返回值  # 市南网站优化公司  # 佛山地产关键词排名  # 南平企业网站优化  # 石峰区营销推广计划  # 衡阳网站seo公司  # 榆林智能营销推广招商  # seo项目怎么做  # 静海区网店如何营销推广  # 槐荫区营销推广保证效果  # 临汾网站建设的渠道  # 中会  # 如何处理  # 创建一个  # 将是  # react  # 重构  # 加载  # 的是  # 多个  # 自定义  # 常见问题  # 应用开发  # ios  # ai  # 工具  # axios  # app  # js 


相关栏目: 【 科技资讯46185 】 【 网络学院92790


相关推荐: 学习通网页版快速入口 学习通官网网页版直接打开  在J*a中如何开发简易仓库管理与库存统计_仓库管理库存统计项目实战解析  内存疯狂猛猛涨价:主板销量直接腰斩!  php源码怎么在电脑上测试_电脑测试php源码方法步骤【教程】  Go语言中动态执行代码字符串的策略与实践  随机参数递归函数的基准调用次数与时间复杂度探究  将HTML动态表格多行数据保存到Google Sheet的教程  C#如何安全地从用户上传的XML文件中读取数据? 验证与清理策略  解决macOS Tkinter应用双击启动崩溃:PyInstaller打包指南  CSS Flexbox与媒体查询:实现响应式布局中元素的并排与堆叠  如何使 Jest 模拟函数默认抛出错误以提高测试效率  Python getattr() 异常处理深度解析:避免程序意外退出  搜狗浏览器如何使用密码生成器创建强密码 搜狗浏览器内置密码安全工具  魅族20怎样在浏览器开无图省流_iPhone魅族20浏览器开无图省流【流量节省】  漫蛙2正版漫画站 漫蛙2网页版快速访问入口  4399网页游戏电脑版全新入口 4399电脑端在线玩指南  AO3官方可用镜像 Archive of Our Own网页版最新入口  处理Kafka消费者会话超时:深入理解消息处理语义与幂等性  Windows10怎么开启夜间模式 Windows10系统设置调整色温与亮度缓解夜间用眼疲劳【教程】  邮编格式怎么匹配地址_根据邮编格式快速匹配详细地址的技巧  Win11怎么设置开机NumLock亮 Win11修改注册表InitialKeyboardIndicators值  Yandex免登录网页版地址 Yandex搜索引擎官方访问入口  Win11如何使用Windows Sandbox Win11沙盒功能开启与使用教程【详解】  顺丰快递查询系统 官方正版查询入口  C++ typeid如何获取类型信息_C++ RTTI运行时类型识别用法  c++如何实现一个简单的ECS框架_c++数据驱动设计与游戏开发  Composer如何处理Git子模块(submodule)依赖_Composer与Git Submodule的对比与选择  Win10系统怎么查看已安装更新_Win10卸载有问题的更新补丁  抓大鹅解压小游戏 抓大鹅摸鱼解压入口  蛙漫2台版漫画地址 Manwa2正版网页版链接  Spyder启动失败:字体文件权限拒绝错误解决方案  steam官方网页快速访问 steam账号注册全流程  b站如何看历史记录_b站观看历史找回方法  AO3中文官网链接_AO3网页版稳定镜像站  cad怎么合并重叠的线段_cad清理重复重叠线条的操作方法  J*aScript中在Map循环中检测并处理空数组元素  Spring Boot嵌入式服务器与J*a EE:功能支持深度解析  C++如何解决segmentation fault_C++段错误调试与原因分析  海棠账号登录入口_登录海棠账户同步阅读记录  为什么我的微信朋友圈看不到别人的更新_微信朋友圈更新显示异常解决方法  Descript怎样用AI剪辑自动去噪_Descript用AI剪辑自动去噪【自动降噪】  哔哩哔哩忘记密码了怎么找回_哔哩哔哩密码找回方法  处理动态列数据:J*a ArrayList的正确初始化与字符累加教程  谷歌邮箱注册显示错误Gmail服务器异常与延迟处理  C++如何连接MySQL数据库_C++使用Connector/C++操作MySQL数据库教程  css滚动动画效果怎么实现_使用Animate.css滚动触发动画类  SteamMachine定价或为699美元 大家想入手吗?  Golang如何使用bytes.Split分割字节切片_Golang bytes切片分割方法  Highcharts 雷达图径向轴标签定制指南:利用多Y轴实现数值标注  Windows 11怎么彻底关闭定位_Windows 11服务中禁用Geolocation 

搜索