MockedProviderでApollo Clientを使ったコンポーネントのテスト

## はじめに

GraphQLの通信部分にApollo Clientを使っている場合、`ApolloProvider`の代わりにApolloの提供している`MockedProvider`を使うことで、GraphQLサーバーに通信をすることなくReactコンポーネントのテストが可能です。

## コンポーネントの例
IDを指定してユーザーを取得し、簡単なテキストを表示するコンポーネントです。

```tsx
import React from 'react';
import { gql, useQuery } from '@apollo/client';

export const GET_USER_QUERY = gql`
  query GetUser($id: ID) {
    user(id: $id) {
      id
      name
      age
    }
  }
`;

export function User({ id }) {
  const { loading, error, data } = useQuery(
    GET_USER_QUERY,
    { variables: { id } }
  );
  if (loading) return <p>Loading...</p>;
  if (error) return <p>An error occurred.</p>;

  return (
    <p>
      {data.user.name} is {data.user.age} years old.
    </p>
  );
}
```

それではテストを追加していきます。

## 1. レスポンスのモックを定義

`request` と `response` フィールドを持つオブジェクトの配列でoperationごとのレスポンスを定義します。

```ts
const mocks = [
  {
    request: {
      query: GET_USER_QUERY,
      variables: {
        id: 1,
      },
    },
    result: {
      data: {
        user: { id: 1, name: 'Tom', age: 25 },
      },
    },
  },
];
```

関数でオブジェクトを返却することも可能

```ts
result: () => {
  // ...何かしらのロジック...

  return {
    data: {
      user: { id: 1, name: 'Tom', age: 25 },
    },
  }
},
```

## 2. 初期のloading状態のテスト
イベントループを待つことなく、すぐにそのままテストを実行すると初期のローディング状態でコンポーネントがレンダリングされます。

```tsx
import TestRenderer from 'react-test-renderer';
import { MockedProvider } from '@apollo/client/testing';

...

it('renders without error', () => {
  const component = TestRenderer.create(
    <MockedProvider mocks={mocks} addTypename={false}>
      <User id={1} />
    </MockedProvider>,
  );

  const tree = component.toJSON();
  expect(tree.children).toContain('Loading...');
});
```
リクエスト時、本来は`__typename`フィールドがクエリに含まれるのですが、テストの際は`addTypename`をfalseにすることでモックのリクエストとレスポンスに`__typename`フィールドを含める必要がなくなります。

## 3. success状態のテスト
setTimeoutしてMockedProviderがモックのレスポンスを取得する処理を走らせます

```tsx
it('should render user', async () => {
  const component = TestRenderer.create(
    <MockedProvider mocks={mocks} addTypename={false}>
      <User id={1} />
    </MockedProvider>,
  );

  // レスポンスを取得
  await new Promise(resolve => setTimeout(resolve, 0));

  const p = component.root.findByType('p');
  expect(p.children.join('')).toContain('Tom is 25 years old.');
});
```

## 4. error状態のテスト
モックにエラーのレスポンスを定義

```ts
const mocks = [
  {
    request: {
      query: GET_USER_QUERY,
      variables: {
        id: 1,
      },
    },
    error: new Error('An error occurred.'),
  },
];
```
successの時と同様にモックのレスポンスを取得

```tsx
it('should render error message', async () => {
  const component = TestRenderer.create(
    <MockedProvider mocks={mocks} addTypename={false}>
      <User id={1} />
    </MockedProvider>,
  );

  // レスポンスを取得
  await new Promise(resolve => setTimeout(resolve, 0));

  const tree = component.toJSON();
  expect(tree.children).toContain('An error occurred');
});
```

## Mutationのテスト

ユーザーを削除するボタンのコンポーネントです。

```tsx
export const DELETE_USER_MUTATION = gql`
  mutation deleteUser($id: ID!) {
    deleteUser(id: $id) {
      id
      name
      age
    }
  }
`;

export function DeleteButton() {
  const [mutate, { loading, error, data }] = useMutation(DELETE_USER_MUTATION);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>An error occurred.</p>;
  if (data) return <p>Deleted.</p>;

  return (
    <button onClick={() => mutate({ variables: { id: 1 } })}>
      Click to Delete Tom
    </button>
  );
}
```

まずはクエリの際と同様にモックのレスポンスを定義

```ts
const mocks = [
  {
    request: {
      query: DELETE_USER_MUTATION,
      variables: {
        id: 1,
      },
    },
    result: {
      data: {
        deleteUser: { id: 1, name: 'Tom', age: 25 },
      },
    },
  },
];
```

`onClick` をトリガーしてMutationを実行

```tsx
it('should delete user', async () => {
  const component = TestRenderer.create(
    <MockedProvider mocks={mocks} addTypename={false}>
      <DeleteButton />
    </MockedProvider>,
  );

  // ボタンをクリック
  const button = component.root.findByType('button');
  button.props.onClick();

  // レスポンスを取得
  await new Promise(resolve => setTimeout(resolve, 0));

  const tree = component.toJSON();
  expect(tree.children).toContain('Deleted!');
});
```

## まとめ

今回はGraphQLサーバーに通信をすることなくReactコンポーネントのテストができるMockedProviderのご紹介でした。