测试 React 组件
使用 MockedProvider 和相关 API
本文介绍了测试使用Apollo 客户端的 React 组件的最佳实践。
以下示例使用Jest 和 React Testing Library,但这些概念适用于任何测试框架。
TheMockedProvider
组件
每个使用 Apollo 客户端 的 React 组件测试都必须在 React 的上下文 中提供 Apollo Client。在应用程序代码中,您通过使用 ApolloProvider
组件来达到这个目的。在您的测试中,您使用 MockedProvider
组件来替代。
MockedProvider 组件允许您定义测试中执行的单一查询的模拟响应。这意味着测试不需要 与GraphQL服务器进行通信,这消除了外部依赖,从而提高了测试的可靠性。
示例
假设我们想测试以下Dog
组件,该组件执行一个基本查询并显示其结果:
该组件的基本渲染测试如下(不包括模拟响应)
import "@testing-library/jest-dom";import { render, screen } from "@testing-library/react";import { MockedProvider } from "@apollo/client/testing";import { GET_DOG_QUERY, Dog } from "./dog";const mocks = []; // We'll fill this in nextit("renders without error", async () => {render(<MockedProvider mocks={mocks} addTypename={false}><Dog name="Buck" /></MockedProvider>);expect(await screen.findByText("Loading...")).toBeInTheDocument();});
注意: 通常,您需要在测试设置文件中导入@testing-library/jest-dom
,这提供了一些自定义 Jest 匹配器(例如toBeInTheDocument
)。导入包含在这些示例中以保证完整性。
定义模拟响应
MockedProvider 的 mocks
prop 是一个对象数组,每个对象定义了单个操作的模拟响应。让我们定义一个对于传递了name
参数Buck
的GET_DOG_QUERY
的模拟响应:
const mocks = [{request: {query: GET_DOG_QUERY,variables: {name: "Buck"}},result: {data: {dog: { id: "1", name: "Buck", breed: "bulldog" }}}}];
每个模拟对象定义了一个 请求
字段(表示与操作相匹配的形状和 变量)和一个 结果
字段(表示返回操作响应的形状)。
您的测试必须执行一个与模拟的形状 完全匹配 的操作,并使用相关的模拟响应。
或者,结果
字段可以是一个函数,在执行任意逻辑之后返回一个模拟响应:
result: (variables) => { // `variables` is optional// ...arbitrary logic...return {data: {dog: { id: '1', name: 'Buck', breed: 'bulldog' },},}},
将上述代码相结合,我们得到以下完整的测试
重用模拟
默认情况下,模拟对象只使用一次。如果您想将模拟对象用于多个 操作,可以将 maxUsageCount
字段设置为一个数字,表示模拟对象应使用的次数:
传递 Number.POSITIVE_INFINITY
将使模拟对象无限期地重用。
动态变量
有时,传递的 变量的确切值 未知。模拟响应对象采用一个 变量 匹配器属性,这是一个函数,它接受变量并返回一个布尔值,表示是否应该为此提供的 查询 匹配此模拟。您不能同时指定此参数和 request.variables
。
例如,此模拟将匹配所有狗查询
import { MockedResponse } from "@apollo/client/testing";const dogMock: MockedResponse<Data, Variables> = {request: {query: GET_DOG_QUERY},variableMatcher: (variables) => true,result: {data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } },},};
这还可以用于单独断言特定的 变量:
import { MockedResponse } from "@apollo/client/testing";const dogMock: MockedResponse<Data, Variables> = {request: {query: GET_DOG_QUERY},variableMatcher: jest.fn().mockReturnValue(true),result: {data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } },},};expect(variableMatcher).toHaveBeenCalledWith(expect.objectContaining({name: 'Buck'}));
设置 addTypename
在上面的示例中,我们将模拟提供者的 addTypename
属性设置为 false
。这可以防止 Apollo 客户端自动将特殊 __typename
字段添加到它查询的每个对象中(它默认这样做以支持缓存中的数据 规范化)。
我们不想在测试中将__typename
自动添加到GET_DOG_QUERY
中,因为这样就不会匹配我们的模拟所期望的查询的形状。
除非您明确配置您的模拟器以期望一个__typename
字段,在测试中总是将addTypename
设置为false
。
测试“加载”和“成功”状态
为了测试组件在其查询完成后如何渲染,Testing Library提供了几个findBy
方法。有关Testing Library文档:
findBy
查询适用于您期望某个元素出现但DOM的变化可能不会立即发生的情况。
我们可以使用异步的screen.findByText
方法首先查询包含加载信息的DOM元素,然后查询成功信息"Buck是一条贵宾犬"}
(在查询完成后出现):
it("should render dog", async () => {const dogMock = {delay: 30 // to prevent React from batching the loading state away// delay: Infinity // if you only want to test the loading staterequest: {query: GET_DOG_QUERY,variables: { name: "Buck" }},result: {data: { dog: { id: 1, name: "Buck", breed: "poodle" } }}};render(<MockedProvider mocks={[dogMock]} addTypename={false}><Dog name="Buck" /></MockedProvider>);expect(await screen.findByText("Loading...")).toBeInTheDocument();expect(await screen.findByText("Buck is a poodle")).toBeInTheDocument();});
测试错误状态
组件的错误状态与成功状态一样重要,如果不是更重要的话,需要测试。您可以使用MockedProvider
组件来模拟网络错误和GraphQL错误。
- 网络错误是客户端在与您的GraphQL服务器通信时发生错误。
- GraphQL错误是在GraphQL服务器解析客户端的操作时发生的错误。
网络错误
要模拟网络错误,您可以在测试的模拟对象中包含一个error
字段,而不是result
字段:
it("should show error UI", async () => {const dogMock = {request: {query: GET_DOG_QUERY,variables: { name: "Buck" }},error: new Error("An error occurred")};render(<MockedProvider mocks={[dogMock]} addTypename={false}><Dog name="Buck" /></MockedProvider>);expect(await screen.findByText("An error occurred")).toBeInTheDocument();});
在这种情况下,当Dog
组件执行其查询时,MockedProvider
返回相应的错误。这使我们的Dog
组件应用错误状态,使我们能够验证错误被优雅地处理。
GraphQL错误
要模拟GraphQL错误,您在模拟的result
字段内部定义一个errors
字段。此字段的值是一个实例化的GraphQLError
对象数组:
const dogMock = {// ...result: {errors: [new GraphQLError("Error!")],},};
因为GraphQL支持在发生错误时返回部分结果,一个模拟对象的result
可以包含errors
字段和data
字段。
测试突变
您测试使用 useMutation
的组件,就像您测试使用 useQuery
的组件一样。就像在您的应用程序代码中一样,主要的 区别 就是您需要调用 mutation's mutate 函数 才能真正执行操作。
示例
以下 DeleteButton
组件执行 DELETE_DOG_MUTATION
来删除我们图中的名为 Buck
的狗(别担心,Buck 会没事的 🐶)
import React from "react";import { gql, useMutation } from "@apollo/client";export const DELETE_DOG_MUTATION = gql`mutation deleteDog($name: String!) {deleteDog(name: $name) {idnamebreed}}`;export function DeleteButton() {const [mutate, { loading, error, data }] = useMutation(DELETE_DOG_MUTATION);if (loading) return <p>Loading...</p>;if (error) return <p>Error!</p>;if (data) return <p>Deleted!</p>;return (<button onClick={() => mutate({ variables: { name: "Buck" } })}>Click to Delete Buck</button>);}
我们可以像测试我们的 Dog
组件一样测试这个组件的初始渲染:
import '@testing-library/jest-dom';import userEvent from '@testing-library/user-event';import { render, screen } from '@testing-library/react';import { MockedProvider } from "@apollo/client/testing";import { DeleteButton, DELETE_DOG_MUTATION } from "./delete-dog";it("should render without error", () => {render(<MockedProvider mocks={[]}><DeleteButton /></MockedProvider>);});
在上面的测试中,因为 mutate 函数没有被调用,所以 DELETE_DOG_MUTATION
没有被执行。
以下测试通过点击按钮执行了 mutation:
it("should render loading and success states on delete", async () => {const deleteDog = { name: "Buck", breed: "Poodle", id: 1 };const mocks = [{request: {query: DELETE_DOG_MUTATION,variables: { name: "Buck" }},result: { data: deleteDog }}];render(<MockedProvider mocks={mocks} addTypename={false}><DeleteButton /></MockedProvider>);// Find the button element...const button = await screen.findByText("Click to Delete Buck");userEvent.click(button); // Simulate a click and fire the mutationexpect(await screen.findByText("Loading...")).toBeInTheDocument();expect(await screen.findByText("Deleted!")).toBeInTheDocument();});
同样,这个例子与上面基于 useQuery 的组件类似,但在渲染完成后有所不同。因为这个组件依赖于按钮点击来触发 mutation,所以我们使用 Testing Library 的 user-event 库的 click
方法来模拟点击。这触发了 mutation,随后测试照常运行。
请记住,mock 的 result
也可以是一个函数,所以您可以在返回结果之前执行任意逻辑(例如设置一个布尔值以指示 mutation 已完成)。
测试 错误状态与测试查询时相同。
使用缓存进行测试
如果您的应用程序设置了任何 缓存配置选项(如 possibleTypes
或 typePolicies
),您应该向 MockedProvider
提供一个设置了相同选项的 InMemoryCache
实例:
const cache = new InMemoryCache({// ...configuration options...})<MockedProvider mocks={mocks} cache={cache}><DeleteButton /></MockedProvider>,
以下示例在其缓存配置中指定了 possibleTypes
和 typePolicies
,这两个都必须在相关测试中指定,以防止意外行为。
测试本地状态
为了正确使用 MockedProvider
测试本地状态,您需要将配置的缓存传递给 MockedProvider
本身。
MockedProvider
在幕后创建自己的 ApolloClient 实例,如下所示:
const {mocks,addTypename,defaultOptions,cache,resolvers,link,showWarnings,} = this.props;const client = new ApolloClient({cache: cache || new Cache({ addTypename }),defaultOptions,link: link || new MockLink(mocks || [], addTypename, { showWarnings }),resolvers,});
因此,如果您正在使用 Apollo Client 2.x 本地解析器,或者 Apollo Client 3.x 类型/字段策略,您必须通知MockedProvider
组件您将对@client
字段做出什么操作。否则,幕后创建的ApolloClient
实例不知道如何处理您的测试。
如果您使用 Apollo Client 2.x 本地解析器,请确保将解析器对象传递给MockedProvider
:
<MockedProvider mocks={mocks} resolvers={resolvers} ...
如果您使用 Apollo Client 3.x 类型/字段策略,请确保将配置的缓存实例(带有您的条目类型策略)传递给MockedProvider
:
<MockedProvider mocks={mocks} cache={cache} ...
如果您正在使用 Apollo Client 2.x 本地解析器,您还需要传递您的解析器映射:
<MockedProvider mocks={mocks} cache={cache} resolvers={resolvers} ...
这是必要的,因为否则,MockedProvider
组件不知道如何解析您的查询中的本地字段。
沙箱示例
要查看如何测试组件的实用示例,请查看 CodeSandbox 上的此项目。