【React.js/Apollo】MockProviderを使ったGraphQL Queryを伴うコンポーネントテストの実装


概要

GraphQLリクエストを伴うコンポーネントテストについて試行錯誤した内容を記事に残したいと思います。

今回の記事では、Queryのテストについて説明します。Mutationのテストはスコープ外です。

今回のサンプルアプリケーション

今回説明するサンプルアプリケーションは以下のリポジトリで共有しております。

https://github.com/Ushinji/rails-react-graphql

利用スタックは以下の通りです。

また、ServerSide(Ruby)とFrontend(React.js,TypeScript)のGraphQLスキーマ情報を共有するために、graphql-code-generatorを利用しています。詳細はこの記事では割愛します。興味がある方は以下の記事が参考になります。

https://qiita.com/saiidalhalawi@github/items/5853904d0bbdcdddfed3

テスト対象のページ

今回、テスト対象となるページは、以下の画像投稿一覧ページです。

GraphQLのQuery(usePostsQuery)で取得した画像投稿をシンプルに一覧表示しています。

import * as React from 'react';
import { usePostsQuery } from '@/graphql/generated/graphql';

const PostListPage: React.VFC = () => {
  const { loading, error, data } = usePostsQuery();

  if (loading) return <div>Loading...</div>;
  if (error || !data) return <div>Error</div>;

  return (
    <ul>
      {data.posts.map((post) => (
        <li key={`post-${post.id}`}>
          <p>{`No.${post.id}: ${post.displayName}`}</p>
          <img src={post.image} alt="post" />
        </li>
      ))}
    </ul>
  );
};

export default PostListPage;

Github上のコードはこちらから確認できます

テストコード

今回のテスト対象のコードは、GraphQLによるAPIリクエストが行われます。この場合、GraphQLリクエストをMockすることが必要です。

往々にしてAPIリクエストのモックが大変なのですが、Apollo ClientではMockedProviderというMock処理が提供されています。これを利用することで、とても簡単にGraphQLリクエストをMockすることができます。

以下に、実際のテストコードを記載します。

import * as React from 'react';
import { act, render } from '@testing-library/react';
import { InMemoryCache } from '@apollo/client';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { PostsDocument } from '@/graphql/generated/graphql';
import PostListPage from '@/containers/PostListPage';

const mocks = [
  {
    request: {
      query: PostsDocument,
    },
    result: {
      data: {
        posts: [
          {
            id: '1',
            image: '/uploads/post/image/1/test1.png',
            displayName: 'TEST_DISPLAY_NAME1',
            createdAt: '2021-09-10T11:00:00Z',
            updatedAt: '2021-09-10T11:00:00Z',
            __typename: 'Post',
          },
          {
            id: '2',
            image: '/uploads/post/image/2/test2.png',
            displayName: 'TEST_DISPLAY_NAME2',
            createdAt: '2021-09-10T12:00:00Z',
            updatedAt: '2021-09-10T12:00:00Z',
            __typename: 'Post',
          },
        ],
      },
    },
  },
];

describe('Loading state', () => {
  it('Snapshot test', () => {
    const { asFragment } = render(
      <MockedProvider
        mocks={mocks}
        defaultOptions={{
          watchQuery: { fetchPolicy: 'no-cache' },
          query: { fetchPolicy: 'no-cache' },
        }}
        cache={new InMemoryCache({ resultCaching: false })}
        addTypename
      >
        <PostListPage />
      </MockedProvider>
    );
    
    expect(asFragment()).toMatchSnapshot();
  });
});

Github上のコードはこちらから確認できます

内容が多いので、それぞれ順を追って説明します。

MockedProviderの利用方法

まず、テスト対象のコンポーネントをrenderする箇所です。テスト対象のPostListPageをrenderする際に、GraphQLリクエストをMockするためにMockedProviderでWrapしています。

const { asFragment } = render(
  <MockedProvider
    mocks={mocks}
    defaultOptions={{
      watchQuery: { fetchPolicy: 'no-cache' },
      query: { fetchPolicy: 'no-cache' },
    }}
    cache={new InMemoryCache({ resultCaching: false })}
    addTypename
  >
    <PostListPage />
  </MockedProvider>
);

MockedProviderの各Propsにオプションを指定することができます。

mocksには、MockしたいGraphQLリクエストの内容を記述します。requestには、Mock対象のQueryパラメタを指定し、resultにはQueryに対するレスポンスを指定します。

補足として、request.queryで指定しているPostsDocumentgraphql-code-generatorで自動生成されるpostsQueryのQuery内容です。

const mocks = [
  {
    request: {
      query: PostsDocument,
      /**
        PostsDocument = gql`query posts {
	  posts {
	    id
	    image
	    displayName
	    createdAt
	    updatedAt
	  }
	}`;
     */
    },
    result: {
      data: {
        posts: [
          {
            id: '1',
            image: '/uploads/post/image/1/test1.png',
            displayName: 'TEST_DISPLAY_NAME1',
            createdAt: '2021-09-10T11:00:00Z',
            updatedAt: '2021-09-10T11:00:00Z',
            __typename: 'Post',
          },
          {
            id: '2',
            image: '/uploads/post/image/2/test2.png',
            displayName: 'TEST_DISPLAY_NAME2',
            createdAt: '2021-09-10T12:00:00Z',
            updatedAt: '2021-09-10T12:00:00Z',
            __typename: 'Post',
          },
        ],
      },
    },
  },
];

また、他のProps(cache, defaultOptions)ではキャッシュをオフにする設定を行なっています。テスト中にApollo Clientのキャッシュが残ってしまうとテストに影響があるので、キャッシュが残らないように設定しています。

GraphQLのリクエスト状態に応じたテスト

今回のテストでは、上述の設定でコンポーネントをrenderした結果をスナップショットテストを行なっています。

この状態でテストを行うとGraphQLクエリをMockできるのですが、GraphQLクエリの結果が画面表示される前にテストが終わってしまうと、Loading状態表示がスナップショットに残ります。

describe('Loading state', () => {
  it('Snapshot test', () => {
    const { asFragment } = render(
      <MockedProvider
        mocks={mocks}
        defaultOptions={{
          watchQuery: { fetchPolicy: 'no-cache' },
          query: { fetchPolicy: 'no-cache' },
        }}
        cache={new InMemoryCache({ resultCaching: false })}
        addTypename
      >
        <PostListPage />
      </MockedProvider>
    );
    
    // GraphQLリクエスト中のため、Loading状態がスナップショットテストに残る
    expect(asFragment()).toMatchSnapshot();
  });
});

そのため、Loading完了後の結果のテストを行いたい場合は、少しSleep処理を追加すれば対応できます。今回は簡単なSleep処理の関数(sleep)を実装して対応しました

const sleep = async (ms: number) => {
  await new Promise((res) => setTimeout(res, ms));
};

describe('Loaded state', () => {
  it('Snapshot test', async () => {
    const { asFragment } = render(
      <MockedProvider
        mocks={mocks}
        defaultOptions={{
          watchQuery: { fetchPolicy: 'no-cache' },
          query: { fetchPolicy: 'no-cache' },
        }}
        cache={new InMemoryCache({ resultCaching: false })}
        addTypename
      >
        <PostListPage />
      </MockedProvider>
    );
    
    // Loadingが完了するまで、少しSleepする
    await act(async () => {
      await sleep(10);
    });
    
    // Loading完了後の結果をスナップショットテストに残す
    expect(asFragment()).toMatchSnapshot();
  });
});

今回のスナップショットテストの結果は以下の通りです。

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`PostListPage Loaded state Snapshot test 1`] = `
<DocumentFragment>
  <ul>
    <li>
      <p>
        No.1: TEST_DISPLAY_NAME1
      </p>
      <img
        alt="post"
        src="/uploads/post/image/1/test1.png"
      />
    </li>
    <li>
      <p>
        No.2: TEST_DISPLAY_NAME2
      </p>
      <img
        alt="post"
        src="/uploads/post/image/2/test2.png"
      />
    </li>
  </ul>
</DocumentFragment>
`;

exports[`PostListPage Loading state Snapshot test 1`] = `
<DocumentFragment>
  <div>
    Loading...
  </div>
</DocumentFragment>
`;

参考文献

https://www.apollographql.com/docs/react/development-testing/testing/#the-mockedprovider-component
https://tomoima525.hatenablog.com/entry/2021/02/22/081942