Redwood 组件测试指南:使用 mockGraphQLQuery 与 mockGraphQLMutation 模拟 GraphQL 请求
2026/9/24 13:20:27 网站建设 项目流程
  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

项目地址:https://gitcode.com/gh_mirrors/re/redwood
点击查看免费下载

在 RedwoodJS 中,不依赖真实后端 API 来测试和构建组件是官方推荐的最佳实践。本文以 version-6.x 的官方文档 为核心骨架,系统讲解 Redwood 提供的一对核心测试工具mockGraphQLQuerymockGraphQLMutation:从操作名匹配、mock 数据与响应上下文(ctx)调整,到 TypeScript 类型约束、Storybook 全局/局部作用域,再到 Cell 的QUERY自动 Mock 机制。读完本文,你将能够在不启动 API 服务的情况下,为任意组件、Story 和 Cell 编写稳定、可复现的 GraphQL Mock,并理解其底层基于 MSW 的实现原理。

为什么要 Mock GraphQL 请求

前端组件(尤其是 Cell 和深度嵌套的组件树)在测试或 Storybook 中渲染时,通常会发起 GraphQL 查询或变更。如果这些请求真实地打到开发服务器或测试环境,会带来三个问题:

  • 不稳定:测试结果依赖后端数据状态,数据一变测试就挂;
  • :每次测试都要等待真实的网络往返;
  • 难覆盖边界:错误响应、慢响应、空数据等场景难以在真实 API 上稳定复现。

Redwood 通过mockGraphQLQuery(匹配查询)和mockGraphQLMutation(匹配变更)让开发者以"声明式"的方式拦截 GraphQL 请求并返回预设数据。两个函数的参数签名完全一致,内部仅根据后缀不同而匹配不同的操作类型(operation type)——这一设计在源码中有直接体现:packages/testing/src/web/mockRequests.ts中,两者都委托给同一个mockGraphQL(type, operation, data)内部函数,区别仅仅是type传入'query'还是'mutation'

核心 API 与基本用法

最基本的用法是传入操作名和 mock 数据:

mockGraphQLQuery('OperationName', (variables, { ctx, req }) => { ctx.delay(1500) // 让响应暂停 1.5 秒 return { userProfile: { id: 42, name: 'peterp', } } })
  • 第一个参数是操作名(operation name)
  • 第二个参数可以是对象函数(函数的返回值作为 mock 数据)。

在 Jest 测试中,这两个函数会被自动挂载为全局函数(见 jest.setup.js 中的global.mockGraphQLQuery = mockGraphQLQuery),因此在测试文件里可以直接调用,无需额外 import。

操作名(Operation Name):Mock 与操作的关联键

第一个参数对应 GraphQL 文档中的操作名,它是将 mock 数据与某个查询或变更关联起来的唯一依据:

query UserProfileQuery { /*...*/ } mockGraphQLQuery('UserProfileQuery', { /*... */ })
mutation SetUserProfile { /*...*/ } mockGraphQLMutation('SetUserProfile', { /*... */ })

操作名必须保持唯一。如果同一个操作名被注册了多个 handler,后注册的会覆盖先注册的(这正是一节"全局 mock 覆盖"机制的基础)。从源码看,mockGraphQL内部调用graphqltype来注册 MSW handler(见 mockRequests.ts),graphql.querygraphql.mutation正是 MSW 按操作类型 + 操作名精确匹配的入口。

Mock 数据:对象或函数

第二个参数支持两种形态:

直接传对象

适用于数据固定、不需要根据变量变化的场景:

mockGraphQLQuery('UserProfileQuery', { userProfile: { id: 42, name: 'peterp', }, })

传函数

函数会收到两个参数:variables(本次请求携带的 GraphQL 变量)和{ ctx, req }。函数返回值作为响应数据:

mockGraphQLQuery('OperationName', (variables, { ctx }) => { ctx.delay(1500) // 暂停 1.5 秒 return { userProfile: { id: 42, name: 'peterp', } } })

借助variables,你可以在同一个操作名下根据不同的入参返回不同数据,从而模拟"按条件查询"的行为。req则暴露了底层 MSW 的请求对象,可以读取请求头、请求原文等(DataFunction的类型签名定义在 mockRequests.ts)。

用 ctx 调整响应:status、delay、errors

当第二个参数是函数时,ctx对象提供了三个内置方法,用于对响应做精细化调整:

方法作用典型场景
ctx.status(code: number, text?: string)设置 HTTP 响应状态码(可附带状态文本)模拟 404、500 等错误状态
ctx.delay(numOfMS)延迟响应指定毫秒数模拟慢网络、验证 loading 态
ctx.errors(e: GraphQLError[])在响应中返回 GraphQL 错误数组模拟服务端校验失败、业务错误

设置 HTTP 状态码

mockGraphQLQuery('OperationName', (_variables, { ctx }) => { ctx.status(404) })

延迟响应

mockGraphQLQuery('OperationName', (_variables, { ctx }) => { ctx.delay(1500) // 暂停 1.5 秒 return { id: 42 } })

配合 Redwood 的 Suspense / Cell 的 loading 态,可以稳定断言"数据加载中"的 UI 表现。

返回 GraphQL 错误

mockGraphQLQuery('OperationName', (_variables, { ctx }) => { ctx.errors([{ message: 'Uh, oh!' }]) })

从源码看,这些方法并非直接透传:mockGraphQL会包装 MSW 的原始ctx,把statusdelayerrors等方法的返回值逐一捕获进responseTransforms数组,最后统一拼进最终的res()调用(见 mockRequests.ts)。这意味着你可以同时使用多个ctx方法(例如先ctx.status(500)ctx.errors([...])),它们会按顺序叠加生效。

TypeScript:为 Mock 提供强类型

Redwood 会自动生成 GraphQL 相关类型(默认位于types/graphql),你可以直接把它们传给 Mock 函数,获得完整的类型检查:

import type { UserProfileQuery, UserProfileQueryVariables } from 'types/graphql' mockGraphQLQuery<UserProfileQuery, UserProfileQueryVariables>('UserProfileQuery', { /*... */ })
  • 第一个泛型参数对应查询/变更的返回数据结构
  • 第二个泛型参数对应变量结构

也可以手动传入自定义类型,适用于类型尚未生成或需要刻意构造"非标准"数据的情况:

mockGraphQLQuery<{ userProfile: { id: number, name: string, } }>('UserProfileQuery', { /*... */ })

源码层面,mockGraphQLQuerymockGraphQLMutation的泛型默认值分别为Record<string, unknown>Record<string, any>(见 mockRequests.ts),因此即使不显式传入类型也不会编译报错,但传入类型后编辑器会针对variables和返回数据给出自动补全与错误提示。

全局 Mock vs 局部 Mock

全局 Mock:.mock.js文件

把 mock 请求放在命名为"<name>.mock.js"(或.mock.ts.mock.jsx.mock.tsx)的文件中,即可在Storybook 中全局生效,对所有 Story 可用:

为什么需要全局 Mock?

在 React 中,一个组件常常内部嵌套了深层组件,而这些嵌套组件会各自发起 GraphQL 查询或变更。如果每个 Story 都要手动 Mock 一遍这些请求,会非常痛苦和繁琐。全局 Mock 一次注册、处处可用。

局部 Mock:Story 内部调用

在 Story 内部直接调用mockGraphQLQuerymockGraphQLMutation则是局部作用域的,并且会覆盖同名的全局 Mock。官方建议:始终优先从全局 Mock 起步,仅在个别 Story 需要特殊数据时再局部覆盖。

该机制在 Jest 测试环境中同样成立:jest.setup.jsbeforeAll阶段自动扫描并加载所有 Cell Mock,随后启动 MSW 服务;在afterEach中调用setupRequestHandlers()重置 handlers,保证用例之间互不污染(见 jest.setup.js)。

Mocking Cell 的 QUERY:.mock.js与 standard 导出

Redwood 的 Cell 是"数据驱动"的核心模式,每个 Cell 组件都导出一个QUERY。要 Mock Cell 的查询,只需要在Cell 所在目录中创建一个同名的.mock.js文件,并导出一个名为standard的值:

export const QUERY = gql` query UserProfileQuery { userProfile { id } } ` // UserProfileCell/UserProfileCell.mock.js export const standard = { userProfile: { id: 42 } }

standard的值就是该 Cell 的QUERY返回的 mock 数据。因此有一个重要的维护约束修改了QUERY,就必须同步修改 mock 数据,否则会出现"查询请求了name字段,但 mock 数据里没有"之类的字段缺失问题:

export const QUERY = gql` query UserProfileQuery { userProfile { id + name } } ` // UserProfileCell/UserProfileCell.mock.js export const standard = { userProfile: { id: 42, + name: 'peterp', } }

幕后机制(Behind the scenes)

Redwood 会把standard的值作为mockGraphQLQuery的第二个参数传入。

这一"幕后机制"比文档描述的更为智能——它是由 Babel 插件在编译期自动完成的,而非运行时手动调用。babel-plugin-redwood-mock-cell-data插件会:

  1. 找到 Cell 目录下导出了standard.mock.*文件;
  2. 解析同目录 Cell 源码中QUERY的 GraphQL 文档,提取出操作名
  3. export const standard = {...}重写为mockGraphQLQuery('<operationName>', standard)的形式;
  4. 若 Cell 还导出了afterQuery,则自动把 mock 数据包进afterQuery(...)再返回。

对应的实现见 babel-plugin-redwood-mock-cell-data.ts,其转换规则在注释中明确列出:必须是*.mock.[ts,js]文件、必须有名为standard的具名导出、必须与 Cell 相邻、Cell 必须有QUERY导出且其操作名可解析。在真实项目夹具中可以看到这类文件的典型形态,例如__fixtures__/example-todo-main/web/src/components/NumTodosCell/NumTodosCell.mock.js

源码级原理:MSW 与懒注册队列

Redwood 的 GraphQL Mock 底层基于MSW(Mock Service Worker)

  • Jest(Node)环境下使用msw/nodesetupServer
  • Storybook(浏览器)环境下使用setupWorker
  • 入口函数startMSW(target, options)会根据目标环境选择对应的 MSW 实现(见 mockRequests.ts)。

一个值得注意的细节是懒注册队列:开发者可以在 MSW 服务启动之前就调用mockGraphQLQuery/mockGraphQLMutation。此时 handler 不会丢失,而是被暂存在REQUEST_HANDLER_QUEUE队列中,等startMSW启动服务后再统一"排空"注册(见 mockRequests.ts)。registerHandler会在服务未启动时追加到队列,服务已启动时直接调用SERVER_INSTANCE.use(handler)

此外,源码还提供了一些文档之外但可直接使用的能力:

  • responseEnhancer第三参数mockGraphQLQuery/mockGraphQLMutation还接受一个可选的响应增强参数('once'表示仅拦截一次、'networkError'表示模拟网络错误),在 MockHandlers.test.tsx 中可以看到'once'的实际用法;
  • mockCurrentUser:通过注册__REDWOOD__AUTH_GET_CURRENT_USER这个特殊操作名的 Mock 来模拟当前登录用户,便于测试依赖useAuth()的组件。

实践建议

  • 能全局就全局:把通用的查询/变更 Mock 放进.mock.js文件,避免每个 Story 重复注册;只有特殊场景才在 Story 内局部覆盖。
  • 保持操作名唯一且语义清晰:操作名既是 GraphQL 规范要求,也是 Mock 匹配的键,命名混乱会导致 Mock 互相覆盖、排查困难。
  • QUERY 与 standard 同步演进:Cell 增加字段后立即同步.mock.js,否则测试与 Storybook 会出现静默的字段缺失。
  • 善用 ctx 覆盖边界场景:用ctx.delay验证 loading 态、用ctx.errors验证失败态、用ctx.status验证 HTTP 错误分支,让组件测试覆盖真实网络环境下难以稳定复现的路径。
  • 充分利用生成的类型:优先从types/graphql引入自动生成的类型来约束 mock 数据,编译器能在数据与查询不同步时第一时间给出提示。

通过mockGraphQLQuerymockGraphQLMutation,Redwood 将"不依赖 API 的前端开发与测试"变成了开箱即用的能力:它既是单元测试的稳定数据源,也是 Storybook 中独立构建组件的基础设施,底层由 MSW 统一接管网络层,让开发者专注于"组件在该数据下如何渲染"这一核心问题。

  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

项目地址:https://gitcode.com/gh_mirrors/re/redwood
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询