【免费下载链接】relay
Relay is a JavaScript framework for building data-driven React applications.
Relay 为构建数据驱动的 React 应用引入了一层数据获取层,这层逻辑让组件测试变得复杂。本文以官方指南《Testing Relay Components》为骨架,系统讲解 relay-test-utils 中 createMockEnvironment 与 MockPayloadGenerator 两大测试模块:从 RelayMockEnvironment 的完整 API 清单、Mock Resolver 的编写规则,到查询 / Fragment / 分页 / Refetch / Mutation / Subscription 六类组件的可复制测试代码。阅读本文后,你将掌握用 Jest 编写简洁、稳定、易于维护的 Relay 组件单元测试与集成测试的完整方法,并理解这些工具在 relay-test-utils 与 Relay 编译器中的底层实现原理。
为什么需要为 Relay 组件编写测试
在开始动手之前,先明确写测试的价值。为 Relay 组件编写 Jest 单元测试(测试单个组件)与集成测试(测试组件组合)有四个直接收益:
- 提升系统稳定性:Flow 等静态检查只能捕获一部分 JavaScript 错误,无法防止组件层面的回归。单元测试能帮助发现、复现、修复回归,并防止其再次出现。
- 简化重构过程:只要测试针对组件的公共接口而非内部实现编写,重构内部实现时测试无需改动,重构信心大增。
- 加速并改善开发流程:本质上就是 TDD——先为组件的公共接口写测试,再实现满足该接口的组件。此时 Jest 的
--watch模式会大放异彩。 - 简化新人上手流程:一套完备的测试让新开发者能快速了解代码库行为,从修 bug、交付功能开始逐步上手。
需要强调的是:Jest 单元/集成测试只是整体稳定性基础设施中的一层,它与 Flow、端到端测试(e2e)、截图测试、"Redbox" 测试、性能测试共同构成多层自动化测试体系。测试工具也不仅限于 Jest,同样的 API 可以用于截图测试、生产冒烟测试、模糊测试等场景。
测试 Relay 组件的两大核心模块
测试使用 Relay 的应用之所以有挑战性,是因为产品代码外围包裹着额外的数据获取层,开发者往往难以把握 Relay 背后的请求/响应机制以及如何正确地与框架交互。
好消息是 relay-test-utils 提供了两套核心模块来简化这个过程(在 index.js 中统一导出):
createMockEnvironment(options): RelayMockEnvironment:创建一个专用于测试的 Relay 环境。它完整实现 Relay Environment 接口,并在其上叠加一层 Mock 层,提供解析 / 拒绝操作、控制查询 / Mutation / Subscription 请求流程的能力。MockPayloadGenerator与@relay_test_operation指令:用于自动生成并维护测试所需的 mock 数据。
这两个模块可由 require('relay-test-utils') 或 import {createMockEnvironment, MockPayloadGenerator} from 'relay-test-utils' 引入。此外该包还额外导出了 unwrapContainer 与 testResolver(详见后文)。
为什么需要 MockPayloadGenerator?在 Relay 组件的测试里存在一个常见痛点:95% 的测试代码是准备阶段——一个巨大的、手工创建的 mock 对象,或是一段复制过来的样例服务器响应,真正的断言只有 5%。结果就是大家不爱写测试:为各种场景手工创建、维护这些哑数据又慢又痛苦。MockPayloadGenerator 与 @relay_test_operation 的目标正是消灭这种模式,把开发者的注意力从"准备测试数据"转移到"真正地测试逻辑"上。
RelayMockEnvironment API 全景
RelayMockEnvironment 是 Relay Environment 的测试专用版本,通过 environment.mock 暴露额外的 API。其实现位于 RelayModernMockEnvironment.js:它内部以 new Environment({configName: 'RelayModernMockEnvironment', network: Network.create(execute, execute), store, ...config}) 构建一个真实环境,再用 execute、executeSubscription、executeWithSource、executeMutation 四个代理拦截所有操作,把它们追加进 pendingOperations 队列——这正是后续所有查询/解析 API 的数据来源。根据内部实现(MAX_SIZE = 10、MAX_TTL = 5 * 60 * 1000),环境自带一个容量 10、TTL 5 分钟的 QueryResponseCache 用于缓存查询响应。
全部方法按用途可分为四组:
查找操作的方法
| 方法 | 说明 |
|---|---|
getAllOperations() | 返回测试进行到当前时刻为止,环境上执行过的所有操作的数组(按发生顺序)。 |
findOperation(findFn => boolean) | 在已执行操作列表中按 findFn 查找特定操作;找不到会抛出异常。当同一时刻有多个操作并发时,用它定位特定操作非常有用。 |
getMostRecentOperation() | 返回最近一次执行的操作;若此前没有执行过任何操作会抛出异常。 |
解析或拒绝操作的方法
| 方法 | 说明 |
|---|---|
nextValue(request \| operation, data) | 为操作提供 payload 但不结束该请求。实际用于测试增量更新与订阅。 |
complete(request \| operation) | 结束操作,此后不再为该操作提供 payload。 |
resolve(request \| operation, data) | 用提供的 GraphQL 响应解析请求。本质上等价于 nextValue(...) + complete(...)。 |
reject(request \| operation, error) | 用指定错误拒绝请求(内部将字符串错误自动包装为 Error)。 |
resolveMostRecentOperation(operation => data) | resolve 与 getMostRecentOperation 的组合:取出最近操作并用 payload(或返回 payload 的回调)解析它。 |
rejectMostRecentOperation(operation => error) | reject 与 getMostRecentOperation 的组合:取出最近操作并用错误(或返回错误的回调)拒绝它。 |
queueOperationResolver(operation => data \| error) | 将一个 OperationResolver 函数加入队列;后续出现在环境上的操作会自动按队列中的 resolver 解析或拒绝(返回 Error 实例即拒绝,返回 payload 即解析)。每个 resolver 消费一个操作后即出队(见 RelayModernMockEnvironment.js 中 execute 的 resolversQueue 逻辑)。 |
queuePendingOperation(query, variables) | 为了让 usePreloadedQuery hook 不挂起,必须配合调用三件事:queueOperationResolver(resolver)、queuePendingOperation(query, variables)、以及在 queuePendingOperation 之后用完全相同的 query 和 variables 调用 preloadQuery(mockEnvironment, query, variables)。底层实现里,queuePendingOperation 会通过 createOperationDescriptor(getRequest(query), variables) 构造并登记一个操作描述符,使后续的 resolver 能命中它。 |
附加工具方法
| 方法 | 说明 |
|---|---|
isLoading(request \| operation) | 操作尚未完成(未 resolve / reject)时返回 true。实现上会同时比对 request 参数、variables 与 cacheConfig。 |
cachePayload(request \| operation, variables, payload) | 将 payload 写入 QueryResponse cache。写入后,对应查询无需经过网络即可直接命中缓存。 |
clearCache() | 清空 QueryResponse cache。 |
实用提示:
resolve/nextValue内部会对 payload 做合法性校验(ensureValidPayload),要求 payload 必须是含data键的对象,否则抛出MockEnvironment(): Expected payload to be an object with a "data" key.的异常——所以请始终传MockPayloadGenerator.generate(...)的输出或手工构造的{data: {...}}。
另外,当 NODE_ENV === 'test' 时,环境的 execute、executeMutation、executeSubscription、applyUpdate、retain、subscribe 等方法会被自动包装为 jest mock(保留原始行为),方便你断言调用次数;environment.mockClear() 会清空所有 mock 调用记录、清空缓存与操作队列。
MockPayloadGenerator 与 @relay_test_operation 指令
MockPayloadGenerator 能根据操作的 selection 自动生成哑数据,大幅简化测试数据的创建与维护。它导出的核心方法是 generate(operation, mockResolvers?, options?),同时提供 generateWithDefer(operation, mockResolvers?, options?) 用于生成包含 @defer / @stream 增量 payload 的完整响应序列(见 RelayMockPayloadGenerator.js 底部的导出)。
generate 的第二个参数是 Mock Resolvers:一个以 GraphQL 类型名(ID、String、User、Comment 等)为键、以"返回该类型默认数据的函数"为值的对象。
一个最简单的 Mock Resolver:
{
ID() {
// 为类型为 ID 的标量字段返回 mock 值
return 'my-id';
},
String() {
// 所有 String 标量字段都会得到这个默认值
return "Lorem Ipsum"
}
}
也可以为对象类型定义 resolver:
{
// 这将成为查询响应中 User 对象的默认值
User() {
return {
id: 4,
name: "Mark",
profile_picture: {
uri: "http://my-image...",
},
};
},
}
Mock Resolver 的上下文(Context)
Mock Resolver 的第一个参数是包含 Mock Resolver Context 的对象,可以根据上下文返回动态值——例如字段的 name 或 alias、selection 中的路径、参数(args)、父类型等。Context 的类型定义(MockResolverContext)同样可以在 RelayMockPayloadGenerator.js 中找到,包含 parentType、name、alias、path、args 五个只读字段。
{
String(context) {
if (context.name === 'zip') {
return '94025';
}
if (context.path != null && context.path.join('.') === 'node.actor.name') {
return 'Current Actor Name';
}
if (context.parentType === 'Image' && context.name === 'uri') {
return 'http://my-image.url';
}
}
}
ID 生成
Mock Resolver 的第二个参数是一个返回递增整数序列的函数,非常适合在测试中生成唯一 ID:
{
// 将依次生成 "my-id-1", "my-id-2", ...
ID(_, generateId) {
return `my-id-${generateId()}`;
},
}
这里 generateId 由 generator 内部维护(createIdGenerator 闭包),每调用一次递增 1。注意:generate 每次调用会创建新的 ID 生成器,所以同一环境内两次分页解析会各自从 1 开始计数——这也是分页测试示例中通过 first-page-id- / second-page-id- 前缀区分两页数据的原因。
Float、Integer、Boolean 等标量
需要特别留意:对于生产环境的查询,Relay 运行时并没有标量字段(如 Boolean、Integer、Float)的完整类型信息。默认情况下 Relay 无法得知 selection 中某个标量字段的类型,也无法得知对象字段的接口类型。因此,在未加 @relay_test_operation 的操作上,这些标量在 Mock Resolver 中会被当作 String 处理。你可以借助 context 依据字段名、alias 等调整返回值。
@relay_test_operation:为 mock 数据注入类型信息
@relay_test_operation 指令解决的就是上述类型信息缺失问题。带该指令的操作会携带额外元数据,其中包含操作 selection 中每个字段的 GraphQL 类型信息(字段类型、是否复数、是否可空、枚举值列表)。这样你就能为 Scalar(不只是 ID 和 String)以及 Abstract 类型定义 Mock Resolver:
{
Float() {
return 123.456;
},
Boolean(context) {
if (context.name === 'can_edit') {
return true;
}
return false;
},
Node() {
return {
__typename: 'User',
id: 'my-user-id',
};
}
}
从源码看编译器侧的实现:@relay_test_operation 的处理位于 test_operation_metadata.rs。该 transform 把 @relay_test_operation 指令转换为 @__metadata 指令,在代码生成阶段以运行时数据的形式输出——元数据键为 relayTestingSelectionTypeInfo,逐路径记录每个 selection 的 type、plural、nullable、enumValues。对应地,运行时侧 RelayMockPayloadGenerator.js 通过 getSelectionMetadataFromOperation(operation) 读取 operation.request.node.params.metadata?.relayTestingSelectionTypeInfo,把它转成 SelectionMetadata 供生成器使用。transform 还支持通过 test_path_regex 限制该指令只在匹配正则的目录下允许使用,避免指令泄漏到生产代码。
默认 Mock Resolver 与生成规则(源码级补充)
即使你不传任何 Mock Resolver,generate 也有内置默认值。从 RelayMockPayloadGenerator.js 的 DEFAULT_MOCK_RESOLVERS 可以看到(用户自定义 resolver 会覆盖这些默认值):
ID:返回`<${parentType}-mock-id-${n}>`形式的字符串(父类型非空时带前缀,如`<User-mock-id-1>`);Boolean:返回false;Int:返回42;Float:返回4.2。
其余未知标量字段的默认值是 `<mock-value-for-field-"${alias ?? name}">` 形式的占位字符串;__typename 字段总是返回当前对象类型名(无类型信息时是 '__MockObject')。此外生成器还有若干值得一提的行为:
- enum 字段:若带类型元数据,会对 resolver 提供的枚举值做大小写不敏感校验,非法值会抛错,并自动纠正为 schema 中的正确拼写(见
_getCorrectDefaultEnum); - 复数(plural)字段:默认生成包含 1 个元素的数组(
Array(1).fill()),并可通过 defaults 数组控制长度; CONDITION字段:根据变量值与passingValue决定是否包含该分支;INLINE_FRAGMENT:负责__typename的填充与具体类型的默认值合并,抽象类型场景下会优先使用具体类型的 resolver 默认值,再回退到抽象类型的默认值;MODULE_IMPORT(3D 模块):可通过 Mock Resolver 提供形如NormalizationSplitOperation的__module_operation值来 mock 模块化(@module)payload,非法结构会抛出含提示的 invariant 错误;mockClientData选项:为true时才会为CLIENT_EXTENSION客户端扩展字段生成数据(默认false)。
实战:六类组件的测试示例
1. 使用 Hooks / QueryRenderer 的 Relay 组件测试
createMockEnvironment 与 MockPayloadGenerator 组合,可以写出针对使用 Relay Hooks 或 QueryRenderer 组件的简洁测试。一个使用 useLazyLoadQuery 或 QueryRenderer 的组件通常有三种状态:Loading、Error、Data Loaded,下面逐一给出测试:
// 假设你有一个使用 useLazyLoadQuery 或 QueryRenderer 的组件
const MyAwesomeViewRoot = require('MyAwesomeViewRoot');
const {
createMockEnvironment,
MockPayloadGenerator,
} = require('relay-test-utils');
test('Loading State', () => {
const environment = createMockEnvironment();
const renderer = ReactTestRenderer.create(
<MyAwesomeViewRoot environment={environment} />,
);
// 这里只验证 spinner 被渲染出来
expect(
renderer.root.find(node => node.props['data-testid'] === 'spinner'),
).toBeDefined();
});
test('Data Render', () => {
const environment = createMockEnvironment();
const renderer = ReactTestRenderer.create(
<MyAwesomeViewRoot environment={environment} />,
);
// 用 ReactTestRenderer.act 包裹,确保组件完全更新到最终状态
ReactTestRenderer.act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation),
);
});
// 此时操作已被解析,查询数据已写入 store
expect(
renderer.root.find(node => node.props['data-testid'] === 'myButton'),
).toBeDefined();
});
test('Error State', () => {
const environment = createMockEnvironment();
const renderer = ReactTestRenderer.create(
<MyAwesomeViewRoot environment={environment} />,
);
ReactTestRenderer.act(() => {
// 用 rejectMostRecentOperation 模拟错误
environment.mock.rejectMostRecentOperation(new Error('Uh-oh'));
});
expect(
renderer.root.find(item => item.props.testID === 'errorMessage'),
).toBeDefined();
});
注意:原指南在 Error State 示例中把
testID写成了赋值(=),正确写法应为比较(===),上文已按正确语义呈现。
2. Fragment 组件测试
上面的 resolveMostRecentOperation 会为所有子 Fragment 容器(pagination、refetch 等)生成数据。但当根组件下挂载很多子 Fragment 组件、而你想单独测试某一个使用 useFragment 的组件时,推荐做法是:用一个 useLazyLoadQuery 的测试组件包一层,让查询展开(spread)目标组件所需的 Fragment:
test('Fragment', () => {
const environment = createMockEnvironment();
const TestRenderer = () => {
const data = useLazyLoadQuery(
graphql`
query TestQuery @relay_test_operation {
myData: node(id: "test-id") {
# 在这里 spread 你要测试的 fragment
...MyFragment
}
}
`,
{},
);
return <MyFragmentComponent myData={data.myData} />
};
const renderer = ReactTestRenderer.create(
<RelayEnvironmentProvider environment={environment}>
<Suspense fallback="Loading...">
<TestRenderer />
</Suspense>
</RelayEnvironmentProvider>
);
ReactTestRenderer.act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation),
);
});
expect(renderer).toMatchSnapshot();
});
注意这里的查询上加上了 @relay_test_operation,从而让 node(抽象接口类型)能被解析出正确的具体类型 __typename,fragment spread 才能生成有效数据。
3. 分页(Pagination)组件测试
分页组件(例如使用 usePaginationFragment)的测试与 Fragment 测试没有本质区别,但我们可以更进一步:真正验证分页行为——点击 loadMore、触发 refetch,然后断言组件状态变化。
test('Pagination Container', () => {
const environment = createMockEnvironment();
const TestRenderer = () => {
const data = useLazyLoadQuery(
graphql`
query TestQuery @relay_test_operation {
myConnection: node(id: "test-id") {
connection {
# 在这里 spread 你要测试的分页 fragment
...MyConnectionFragment
}
}
}
`,
{},
);
return <MyPaginationContainer connection={data.myConnection.connection} />
};
const renderer = ReactTestRenderer.create(
<RelayEnvironmentProvider environment={environment}>
<Suspense fallback="Loading...">
<TestRenderer />
</Suspense>
</RelayEnvironmentProvider>
);
ReactTestRenderer.act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation, {
ID(_, generateId) {
// 为什么要这样做?
// 确保第一页和第二页的元素生成不同的 ID 集合
return `first-page-id-${generateId()}`;
},
PageInfo() {
return {
has_next_page: true,
};
},
}),
);
});
// 找到 loadMore 按钮并点击,发起分页请求
const loadMore = renderer.root.find(node => node.props['data-testid'] === 'loadMore')
expect(loadMore.props.disabled).toBe(false);
loadMore.props.onClick();
ReactTestRenderer.act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation, {
ID(_, generateId) {
// 注意,第二页的 ID 会不同
return `second-page-id-${generateId()}`;
},
PageInfo() {
return {
// 此时按钮应该(大概)被禁用
has_next_page: false,
};
},
}),
);
});
expect(loadMore.props.disabled).toBe(true);
});
这个例子的精髓在于:通过自定义 ID resolver 让两页数据拥有不同的 ID(否则数据合并时第二页会被去重),通过 PageInfo resolver 控制 has_next_page 的翻转来断言按钮禁用状态的切换。
4. Refetch 组件测试
Refetch 组件(如 useRefetchableFragment)采用类似的"查询包裹"思路,这里给出完整示例:
test('Refetch Container', () => {
const environment = createMockEnvironment();
const TestRenderer = () => {
const data = useLazyLoadQuery(
graphql`
query TestQuery @relay_test_operation {
myData: node(id: "test-id") {
# 在这里 spread 你要测试的可 refetch fragment
...MyRefetchableFragment
}
}
`,
{},
);
return <MyRefetchContainer data={data.myData} />
};
const renderer = ReactTestRenderer.create(
<RelayEnvironmentProvider environment={environment}>
<Suspense fallback="Loading...">
<TestRenderer />
</Suspense>
</RelayEnvironmentProvider>
);
ReactTestRenderer.act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation),
);
});
// 假设 Container 里有 refetch 按钮
const refetchButton = renderer.root.find(node => node.props['data-testid'] === 'refetch');
// 触发 refetch
refetchButton.props.onClick();
ReactTestRenderer.act(() => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation, {
// 可以在这里自定义 mock resolvers,改变 refetch 查询的输出
}),
);
});
expect(renderer).toMatchSnapshot();
});
5. Mutation 测试
Mutation 本身也是操作,因此既可以独立单元测试某个 mutation,也可以与触发它的视图组合测试。指南特别提示:useMutation API 是直接调用 commitMutation 的改进版本,新代码优先使用前者。
// 假设你有一个 mutation 函数
function sendMutation(environment, onCompleted, onError, variables)
commitMutation(environment, {
mutation: graphql`...`,
onCompleted,
onError,
variables,
});
}
test('it should send mutation', () => {
const environment = createMockEnvironment();
const onCompleted = jest.fn();
sendMutation(environment, onCompleted, jest.fn(), {});
const operation = environment.mock.getMostRecentOperation();
ReactTestRenderer.act(() => {
environment.mock.resolve(
operation,
MockPayloadGenerator.generate(operation)
);
});
expect(onCompleted).toBeCalled();
});
注意这里用的是 resolve(operation, payload) 而不是 resolveMostRecentOperation:因为 getMostRecentOperation 已经显式取出了操作,直接用 resolve 即可,代码意图更清晰。
6. Subscription 测试
订阅的测试方式与 Mutation 几乎一致,区别在于使用 nextValue 提供增量 payload(请求不结束),并断言 onNext 回调。同理,指南提示 useSubscription API 是直接调用 requestSubscription 的改进版本。
// 示例 subscribe 函数
function subscribe(environment, onNext, onError, variables)
requestSubscription(environment, {
subscription: graphql`...`,
onNext,
onError,
variables,
});
}
test('it should subscribe', () => {
const environment = createMockEnvironment();
const onNext = jest.fn();
subscribe(environment, onNext, jest.fn(), {});
const operation = environment.mock.getMostRecentOperation();
ReactTestRenderer.act(() => {
environment.mock.nextValue(
operation,
MockPayloadGenerator.generate(operation)
);
});
expect(onNext).toBeCalled();
});
7. 用 queueOperationResolver 预置响应
queueOperationResolver 允许你预先定义环境上将要执行操作的响应,适合多个操作连续发生、不想逐个手动 resolve 的场景。resolver 返回 Error 实例即拒绝操作,返回 payload 即解析操作:
// 假设你有一个使用 QueryRenderer 的组件
const MyAwesomeViewRoot = require('MyAwesomeViewRoot');
const {
createMockEnvironment,
MockPayloadGenerator,
} = require('relay-test-utils');
test('Data Render', () => {
const environment = createMockEnvironment();
environment.mock.queueOperationResolver(operation =>
MockPayloadGenerator.generate(operation),
);
const renderer = ReactTestRenderer.create(
<MyAwesomeViewRoot environment={environment} />,
);
// 此时操作已被解析,查询数据已写入 store
expect(
renderer.root.find(node => node.props['data-testid'] === 'myButton'),
).toBeDefined();
});
test('Error State', () => {
const environment = createMockEnvironment();
environment.mock.queueOperationResolver(() =>
new Error('Uh-oh'),
);
const renderer = ReactTestRenderer.create(
<MyAwesomeViewRoot environment={environment} />,
);
expect(
renderer.root.find(item => item.props.testID === 'errorMessage'),
).toBeDefined();
});
8. Relay Hooks 与 queuePendingOperation
本指南中的所有示例既适用于 Relay Hooks(useLazyLoadQuery、useFragment、usePaginationFragment、useRefetchableFragment、usePreloadedQuery 等),也适用于旧的 Containers(createFragmentContainer 等)与 Renderers(QueryRenderer)。唯一例外是涉及 usePreloadedQuery 的测试:必须按上文 API 表格中 queuePendingOperation 一节的要求,依次调用 queueOperationResolver、queuePendingOperation(query, variables),并在之后用相同参数调用 preloadQuery(mockEnvironment, query, variables),否则组件会一直挂起。
关于 toMatchSnapshot 与 React Testing Library
示例中的断言统一使用了 toMatchSnapshot(),这只是为了让示例更简洁,它并不是推荐的做法。更推荐使用 React Testing Library 这类帮助库来测试 React 组件:它不依赖组件实现细节,让重构变得轻松,同时引导你遵循无障碍(accessibility)最佳实践。虽然 React Testing Library 本身不提供"浅渲染"(不渲染子组件)的能力,但 Jest 这类测试运行器可以通过模块 mock 实现同样效果。
其他实用工具与示例参考
testResolver(RelayResolverTestUtils.js):用于单测 Relay Resolver 的辅助函数。传入 resolver 函数和readFragment应返回的 fragment 数据,即可直接得到 resolver 的派生结果。由于 Relay fragment 数据包含普通代码无法构造的$fragmentType键,测试中可用$fragmentType: (null: any)(配合 Flow suppression)绕过。unwrapContainer(unwrapContainer.js):解包createFragmentContainer等包裹的容器组件,拿到原始组件便于浅渲染测试。
仓库内提供了大量可直接参考的示例测试:
- RelayMockEnvironment-test.js:环境 API 的单元测试;
- RelayMockEnvironmentWithComponents-test.js:包含 QueryRenderer、Fragment Container、Pagination Container、Refetch Container、Mutation、Subscription 等完整组件级测试(如
should resolve query、should reject query、getMostRecentOperation断言操作名与 variables 等); - RelayMockPayloadGenerator-test.js:MockPayloadGenerator 各类型字段生成行为的测试;
- RelayResolverTestUtils-test.js:
testResolver的用法示例。
小结
测试是有益的,你应该坚持为 Relay 组件写测试。回顾本文的核心要点:
createMockEnvironment负责流程控制:通过environment.mock上的resolve/reject/nextValue/complete/getMostRecentOperation/queueOperationResolver/queuePendingOperation等 API,你可以精确控制查询、Mutation、订阅的请求/响应节奏,模拟加载、成功、失败三种状态,甚至逐页推进分页数据。MockPayloadGenerator负责数据生成:结合@relay_test_operation指令注入的 selection 类型元数据,为任意操作自动生成类型正确的 mock 数据;再通过 Mock Resolver(基于类型名、context、generateId)按需定制字段值。- 测试策略:为单个 Fragment / 分页 / Refetch 组件测试时,用
useLazyLoadQuery查询包裹目标组件;Mutation 与 Subscription 可独立单元测试;涉及usePreloadedQuery时记得配合queuePendingOperation;断言上尽量用 React Testing Library 而非过度依赖快照。
这套方法论不仅适用于 Jest 单元/集成测试,其控制流 API 同样可以复用于截图测试、冒烟测试、e2e 测试等更广泛的自动化测试场景,是 Relay 应用稳定性基础设施中的关键一环。
【免费下载链接】relay
Relay is a JavaScript framework for building data-driven React applications.
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



