- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
在 RedwoodJS 中,不依赖真实后端 API 来测试和构建组件是官方推荐的最佳实践。本文以 version-6.x 的官方文档 为核心骨架,系统讲解 Redwood 提供的一对核心测试工具mockGraphQLQuery与mockGraphQLMutation:从操作名匹配、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.query与graphql.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,把status、delay、errors等方法的返回值逐一捕获进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', { /*... */ })源码层面,mockGraphQLQuery与mockGraphQLMutation的泛型默认值分别为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 内部直接调用mockGraphQLQuery或mockGraphQLMutation则是局部作用域的,并且会覆盖同名的全局 Mock。官方建议:始终优先从全局 Mock 起步,仅在个别 Story 需要特殊数据时再局部覆盖。
该机制在 Jest 测试环境中同样成立:jest.setup.js在beforeAll阶段自动扫描并加载所有 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插件会:
- 找到 Cell 目录下导出了
standard的.mock.*文件; - 解析同目录 Cell 源码中
QUERY的 GraphQL 文档,提取出操作名; - 把
export const standard = {...}重写为mockGraphQLQuery('<operationName>', standard)的形式; - 若 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/node的setupServer; - 在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 数据,编译器能在数据与查询不同步时第一时间给出提示。
通过mockGraphQLQuery与mockGraphQLMutation,Redwood 将"不依赖 API 的前端开发与测试"变成了开箱即用的能力:它既是单元测试的稳定数据源,也是 Storybook 中独立构建组件的基础设施,底层由 MSW 统一接管网络层,让开发者专注于"组件在该数据下如何渲染"这一核心问题。
- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
相关推荐
Redwood 组件测试指南:用 mockGraphQLQuery / mockGraphQLMutation 模拟 GraphQL 请求
Redwood 组件测试指南:用 mockGraphQLQuery / mockGraphQLMutation 模拟 GraphQL 请求 测试与构建组件时,不
后端前端Web框架开发工具Redwood 中 Mock GraphQL 请求:用 mockGraphQLQuery / mockGraphQLMutation 测试组件
Redwood 中 Mock GraphQL 请求:用 mockGraphQLQuery / mockGraphQLMutation 测试组件 在 Redwoo
后端前端Web框架开发工具Redwood 测试与 Storybook 中的 GraphQL 请求 Mock 完整指南:mockGraphQLQuery 与 mockGraphQLMutation 实战
Redwood 测试与 Storybook 中的 GraphQL 请求 Mock 完整指南:mockGraphQLQuery 与 mockGraphQLMuta
后端前端Web框架开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考