Relay 组件测试完整指南:createMockEnvironment 与 MockPayloadGenerator 实战

  • 前端
  • 开发工具

【免费下载链接】relay

Relay is a JavaScript framework for building data-driven React applications.

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

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 等包裹的容器组件,拿到原始组件便于浅渲染测试。

仓库内提供了大量可直接参考的示例测试:

小结

测试是有益的,你应该坚持为 Relay 组件写测试。回顾本文的核心要点:

  1. createMockEnvironment 负责流程控制:通过 environment.mock 上的 resolve / reject / nextValue / complete / getMostRecentOperation / queueOperationResolver / queuePendingOperation 等 API,你可以精确控制查询、Mutation、订阅的请求/响应节奏,模拟加载、成功、失败三种状态,甚至逐页推进分页数据。
  2. MockPayloadGenerator 负责数据生成:结合 @relay_test_operation 指令注入的 selection 类型元数据,为任意操作自动生成类型正确的 mock 数据;再通过 Mock Resolver(基于类型名、context、generateId)按需定制字段值。
  3. 测试策略:为单个 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.

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

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

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值