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

如何测试包含多个 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.jsimport { 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 时,开发者可能遇到以下问题:

测试隔离性不足: 多个测试用例之间共享模拟(mock)状态,导致前一个测试的模拟影响后一个测试。例如,在一个测试中只模拟了 getTestByUid,而另一个测试依赖于 getTestStatusesByUid,此时未被模拟的 API 调用可能返回 undefined。模拟值结构不正确: useQuery Hook 的 data 字段直接包含 API 调用返回的数据。如果 API 模拟返回的是 { data: actualData } 这样的嵌套结构,那么 useQuery 最终得到的 data 将是 { data: actualData } 而非 actualData,导致断言失败。冗余的测试用例: 将 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.jsimport * as testApi from './api'; // 引入 API 模块jest.mock('./api'); // 在文件顶部模拟整个 API 模块

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

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

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.jsimport { 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.jsimport { 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, // 在测试中禁用重试,避免不必要的等待    },  },});// 创建一个包装器组件,用于提供 QueryClientProviderconst wrapper = ({ children }) => {  return (    {children}  );};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([]);  });});

注意事项与总结

全局模拟与局部模拟: jest.mock(‘./api’) 是全局模拟,它替换了整个模块。在每个测试用例中,使用 testApi.getTestByUid.mockResolvedValue(…) 则是对模拟模块中特定函数的行为进行局部配置。这种组合是测试异步 Hook 的强大模式。QueryClientProvider: 确保你的测试环境包裹在 QueryClientProvider 中,因为 useQuery 依赖于它。waitForNextUpdate: renderHook 返回的 waitForNextUpdate 是等待 Hook 内部的异步更新完成的关键。对于多个 useQuery 调用,一次 await waitForNextUpdate() 通常足以等待所有初始查询完成,因为 react-query 会在所有依赖项就绪后进行一次渲染。断言类型: 对于对象和数组的比较,请使用 toEqual() 而不是 toBe(),因为 toBe() 检查的是引用相等性,而 toEqual() 检查的是值相等性。错误处理: 编写测试来验证 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的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1527381.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
解决SVG tspan getBBox() 在Firefox中返回错误值的方案
上一篇 2025年12月20日 19:22:20
怎样使用Web Audio API处理和分析音频数据?
下一篇 2025年12月20日 19:22:36

相关推荐

  • 解决三大痛点!三翼鸟建博会升级AI智慧家

    解决三大痛点!三翼鸟建博会升级AI智慧家解决三大痛点!三翼鸟建博会升级AI智慧家解决三大痛点!三翼鸟建博会升级AI智慧家解决三大痛点!三翼鸟建博会升级AI智慧家

    迈入7月,广州进入闷热潮湿的后汛期,走在街头能明显感受到那种令人不适的湿热。尽管如此,并未阻挡来自全国各地客户前往广州的脚步,因为一年一度的建博会正火热进行中。 许多参观者都是抱着“取经”的目的而来。毕竟这里几乎汇聚了中国大家居建装全产业链上的头部品牌。换句话说,想要了解打造理想家居的最前沿方案,来…

    2026年8月29日 用户投稿
    100
  • 怎么关闭苹果自动续费会员

    第一步:确认当前订阅内容 在停用自动续费功能前,需先明确自己已开通了哪些订阅服务。请按以下流程在苹果设备上查看: 1. 登录Apple ID:确保您当前使用的Apple ID为需要管理账户的账号。 2. 打开“设置”应用:在主屏幕找到“设置”图标并点击进入。 3. 进入账户信息:在设置页面顶部,点击…

    2026年8月29日
    000
  • 123网盘下载链接打不开怎么办_123网盘下载链接打不开解决

    先确认链接是否正确,再尝试更换浏览器或清除缓存;若仍无效,可能是链接失效或被封,建议联系分享者重新发送或使用123网盘客户端下载。 123网盘下载链接打不开,可能是由多种原因造成的。别急,先确认问题出在哪一步,再对症处理,基本上都能解决。 检查链接是否正确 确保你输入或点击的链接完整无误,没有遗漏字…

    2026年8月29日
    100
  • 本地环境下如何快速搭建 Yii 开发框架?

    在本地环境下快速搭建 yii 开发框架可以通过 composer 安装和配置 yii 基本应用模板来实现。具体步骤包括:1)安装 composer,使用命令 php -r “copy(‘https://getcomposer.org/installer’, &#8…

    2026年8月29日
    100
  • 小米16 Pro假想图曝光:背面加入副屏 镜头模组大变

    近日,有科技博主曝光了小米即将推出的旗舰产品——小米16 pro的概念设计图。从流出的图片来看,该机背部的相机模块由以往常见的方形造型变为了矩形结构,并在内部进行了全新布局:左侧设有两枚摄像头(其中一枚为潜望式长焦镜头),其下方新增一枚镜头单元。更为亮眼的是,整个矩形模组中嵌入了一块副屏,其功能设想…

    2026年8月29日
    100
  • 如何解决配置文件管理混乱问题?使用hassankhan/config库可以!

    可以通过一下地址学习composer:学习地址 在开发过程中,管理配置文件是一个常见但容易被忽视的挑战。特别是当项目规模扩大,配置文件的种类和数量增加时,如何高效地管理这些文件变得尤为重要。我在处理一个大型项目时,遇到了多种格式的配置文件(如 php、ini、xml、json 和 yaml)需要统一…

    用户投稿 2026年8月29日
    100
  • 三星Z Fold 7及Z Flip 7今日发布 还有手表、头显等新品

    cnmo获悉,三星galaxy全球新品发布会定于7月9日22:00召开,届时三星z fold 7与z flip 7将同步登场。传闻中的z fold ultra和z flip 7 fe也有可能一同亮相。7月8日,三星官方宣布了手机品牌大使,相关信息显示该代言人很可能是徐明浩。 三星Z Fold 7渲染…

    2026年8月29日
    100
  • 悟空浏览器怎么看完整的电视剧

    首先使用悟空浏览器内置搜索框输入剧名,系统将聚合优酷、腾讯视频、爱奇艺等正规平台资源,点击结果可跳转至对应官网播放页面;其次可通过浏览器内的“追剧”或“影视”专区浏览分类整理的热门剧集,选择后同样跳转至合作正版片源平台播放,需确认最终播放页面为官方域名以保障观看完整性与合法性。 想用悟空浏览器看完整…

    2026年8月28日
    200
  • Symfony 框架结合 Workerman,打造高性能 Web 应用的实践案例

    symfony 和 workerman 可以结合使用来打造高性能 web 应用。1) 独立运行 workerman 服务,处理实时通信需求。2) 通过 symfony 的内核事件监听器或命令行工具,将 workerman 集成到 symfony 应用中,实现无缝通信。 引言 在当今的 Web 开发领…

    2026年8月28日
    000
  • 免费获取PPT模板 免费在线演示文稿生成平台

    推荐扑奔网、第一PPT、优品PPT、OfficePlus和稻壳儿获取免费高质量模板,比格AIPPT、笔灵PPT、秒出PPT、讯飞智文、轻竹AIPPT支持AI在线生成,适合不同场景下的PPT制作需求。 想找免费的PPT模板和能在线生成演示文稿的平台,其实有不少好用的选择。重点是找那些真正免费、资源靠谱…

    2026年8月28日
    200
  • EVNIA弈威双核电竞显示器24M2N5200X,搭载610Hz超高刷新率

    EVNIA弈威双核电竞显示器24M2N5200X,搭载610Hz超高刷新率EVNIA弈威双核电竞显示器24M2N5200X,搭载610Hz超高刷新率EVNIA弈威双核电竞显示器24M2N5200X,搭载610Hz超高刷新率EVNIA弈威双核电竞显示器24M2N5200X,搭载610Hz超高刷新率

    在fps游戏的紧张对决中,每一次微小的延迟都可能左右战局走向。对于追求极致操作响应的玩家而言,显示器的性能已成为决定竞技水平的关键因素。evnia弈威霹雳x2系列610hz超高刷新率双核电竞显示器24m2n5200x,凭借其顶尖的技术规格与创新设计理念,已然成为fps玩家提升反应速度与操控精度的“实…

    2026年8月28日 用户投稿
    000
  • Android WebView加载支付宝链接失败,如何解决net::ERR_UNKNOWN_URL_SCHEME问题?

    Android WebView加载支付宝链接失败:net::ERR_UNKNOWN_URL_SCHEME问题详解及解决方案 在Android开发中,使用WebView加载包含自定义URL scheme的链接(例如支付宝的alipays://)时,常常遇到net::ERR_UNKNOWN_URL_SC…

    2026年8月28日
    100
  • 入门爬虫,不讲道理,只摆问题

    入门爬虫,不讲道理,只摆问题入门爬虫,不讲道理,只摆问题入门爬虫,不讲道理,只摆问题入门爬虫,不讲道理,只摆问题

    其实我也算是入门爬虫,目前也还有很多东西没有吃透,比如很多人入门选择使用的正则式我就没记清楚,对于很多反扒也并不算特别深入。但这并不影响我学习爬虫的信心和兴趣。。。没办法,必须要学啊。很多数据我不能跪着求别人给,因为别人不会给。。。被逼着学习爬虫,希望我的学习能有好结果吧 import json i…

    2026年8月28日 用户投稿
    000
  • 夸克搜索字体大小看不清怎么调整_夸克搜索字体大小调整设置方法

    可通过夸克设置、页面手势缩放或系统显示调整改善字体大小问题。一、在夸克【我的】-【设置】-【通用】-【字体大小】中调节;二、浏览网页时用双指张开放大或捏合缩小;三、手机【设置】-【显示与亮度】-【显示大小】调整系统缩放。 如果您在使用夸克搜索时,发现网页或界面字体过小或过大导致阅读困难,可以通过调整…

    2026年8月28日
    200
  • Swoole 协程上下文管理及数据传递的最佳实践

    swoole 协程上下文管理和数据传递的最佳实践包括:1) 使用 swoolecoroutine::getcontext() 和 swoolecoroutine::setcontext() 方法管理上下文;2) 避免频繁读写上下文数据;3) 使用轻量级数据结构存储数据。这些方法有助于在协程间有效传递…

    2026年8月28日
    000
  • win11怎么解决磁盘占用100%的问题_Win11磁盘占用100%的有效解决方案

    首先通过任务管理器定位高磁盘占用进程,结束非关键程序;接着禁用SysMain和Windows Search服务以减少后台读写;暂停Windows Update减轻负载;运行chkdsk检查并修复磁盘错误;最后使用CrystalDiskInfo、TreeSize Free等工具分析硬盘健康与空间占用情…

    2026年8月28日
    000
  • 业绩坠崖,股价狂飙,DeepSeek是用友网络的“华佗”

    互联网江湖风云变幻,用友网络的现状引发诸多关注。雷军曾言“站在风口,猪都能飞”,而如今,在deepseek这股东风的助力下,企业软件服务商们似乎也体验了一把飞翔的快感。然而,用友网络却面临着严峻的挑战。 用友网络与金蝶国际,两岸龙头,近期走势惊人相似:在连续四年下跌后,用友网络年初至今股价上涨55.…

    2026年8月28日
    000
  • 如何用Gemini规划健身计划_Gemini制定个性化健身方案

    答案:Gemini可辅助制定健身计划,但需用户提供详细目标、经验及限制,如具体减重目标、运动偏好和身体状况,以便生成个性化方案。它能提供力量训练、有氧运动、饮食建议和拉伸计划,并支持数据追踪与分析,帮助评估进展。然而,其建议仅为参考,存在无法替代专业教练、信息偏差和忽视个体差异等局限。使用者应关注身…

    2026年8月28日
    000
  • VSCode怎么更改鼠标颜色_VSCode自定义鼠标指针颜色与样式教程

    答案:可通过修改VSCode设置自定义光标颜色、样式、粗细及闪烁方式以提升编码体验。具体包括在settings.json中配置editor.cursorStyle、editor.cursorWidth、editor.cursorBlinking,以及通过workbench.colorCustomiz…

    2026年8月28日
    000
  • 避免 jQuery AJAX POST 请求重复提交的策略与实践

    本文探讨了在使用 jQuery AJAX 进行 POST 请求时,如何有效避免因事件监听器、快速点击或意外行为导致的重复提交问题。我们将介绍一种基于状态标志的解决方案,通过控制请求的执行时机,确保数据提交的准确性和一致性,并提供相应的代码示例和最佳实践建议,以优化用户体验和系统稳定性。 问题描述:A…

    2026年8月28日
    100

发表回复

登录后才能评论
关注微信