10月8日至10日加入我们,在纽约市了解关于GraphQL联合和API平台工程的最新技巧、趋势和新闻。加入我们参加2024年纽约市的GraphQL峰会
文档
免费开始

测试 React 组件

使用 MockedProvider 和相关 API


本文介绍了测试使用的 React 组件的最佳实践。

以下示例使用JestReact Testing Library,但这些概念适用于任何测试框架。

TheMockedProvider 组件

每个使用 Apollo 客户端 的 React 组件测试都必须在 React 的上下文 中提供 Apollo Client。在应用程序代码中,您通过使用 ApolloProvider 组件来达到这个目的。在您的测试中,您使用 MockedProvider 组件来替代。

MockedProvider 组件允许您定义测试中执行的单一查询的模拟响应。这意味着测试不需要GraphQL服务器进行通信,这消除了外部依赖,从而提高了测试的可靠性。

示例

假设我们想测试以下Dog 组件,该组件执行一个基本并显示其结果:

该组件的基本渲染测试如下(不包括模拟响应)

dog.test.js
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 next
it("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)。导入包含在这些示例中以保证完整性。

定义模拟响应

MockedProvidermocks prop 是一个对象数组,每个对象定义了单个的模拟响应。让我们定义一个对于传递了name参数BuckGET_DOG_QUERY的模拟响应:

dog.test.js
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 state
request: {
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服务器解析客户端的操作时发生的错误。

网络错误

要模拟网络错误,您可以在测试的模拟对象中包含一个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 的组件一样。就像在您的应用程序代码中一样,主要的 区别 就是您需要调用 's mutate 函数 才能真正执行操作。

示例

以下 DeleteButton 组件执行 DELETE_DOG_MUTATION 来删除我们图中的名为 Buck 的狗(别担心,Buck 会没事的 🐶)

delete-dog.jsx
import React from "react";
import { gql, useMutation } from "@apollo/client";
export const DELETE_DOG_MUTATION = gql`
mutation deleteDog($name: String!) {
deleteDog(name: $name) {
id
name
breed
}
}
`;
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 组件一样测试这个组件的初始渲染:

delete-dog.test.js
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

delete-dog.test.js
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 mutation
expect(await screen.findByText("Loading...")).toBeInTheDocument();
expect(await screen.findByText("Deleted!")).toBeInTheDocument();
});

同样,这个例子与上面基于 useQuery 的组件类似,但在渲染完成后有所不同。因为这个组件依赖于按钮点击来触发 mutation,所以我们使用 Testing Library 的 user-event 库的 click 方法来模拟点击。这触发了 mutation,随后测试照常运行。

请记住,mock 的 result 也可以是一个函数,所以您可以在返回结果之前执行任意逻辑(例如设置一个布尔值以指示 mutation 已完成)。

测试 错误状态与测试查询时相同。

使用缓存进行测试

如果您的应用程序设置了任何 缓存配置选项(如 possibleTypestypePolicies),您应该向 MockedProvider 提供一个设置了相同选项的 InMemoryCache 实例:

const cache = new InMemoryCache({
// ...configuration options...
})
<MockedProvider mocks={mocks} cache={cache}>
<DeleteButton />
</MockedProvider>,

以下示例在其缓存配置中指定了 possibleTypestypePolicies,这两个都必须在相关测试中指定,以防止意外行为。

测试本地状态

为了正确使用 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 上的此项目。

Edit Testing React Components

上一页
使用 TypeScript
下一页
基于模式测试
评分文章评分

©2024Apollo Graph Inc.,商业名称为Apollo GraphQL。

隐私政策

公司