📝

【クエリ編】ApolloClientを使っているReactアプリのテストが書きたいんだ

2022/10/30に公開

https://www.apollographql.com/docs/react/development-testing/testing/

上記公式ドキュメントに、Apollo Client を使用する React コンポーネントをテストするためのベストプラクティスが載っているので、こちらを基にテストを描いてみました。

クエリを使用するコンポーネントの基本的なテスト

テストするSearchコンポーネント

Propsとして受け取るidを基に、ツイートを検索して、ツイート内容を表示するという簡単なコンポーネントとなります。

Search.tsx
import { useQuery } from "@apollo/client";
import React, { FC } from "react";
import { GET_TWEET_BY_ID } from "../gql/crud";

interface Props {
  id: string;
}

export const Search: FC<Props> = ({ id }) => {
  const { loading, error, data } = useQuery(GET_TWEET_BY_ID, {
    variables: {
      id,
    },
  });

  return (
    <>
      {loading ? (
        <div>ローディング中です。。。</div>
      ) : error ? (
        <div>エラーです</div>
      ) : data.tweets ? (
        <div>
          <table border={1}>
            <tbody>
              <tr>
                <th>id</th>
                <td>{data.tweets.id}</td>
              </tr>
              <tr>
                <th>tweet_content</th>
                <td>{data.tweets.content}</td>
              </tr>
            </tbody>
          </table>
        </div>
      ) : (
        <div>データがありませんでした、、、</div>
      )}
    </>
  );
};

テストコード

以下が書いてみたテストになります。

search.test.tsx
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import { MockedProvider } from "@apollo/client/testing";
import { Search } from "../components/Search";
import { GET_TWEET_BY_ID } from "../gql/crud";

//モックの定義
const mocks = [
  {
    request: {
      query: GET_TWEET_BY_ID,
      variables: {
        id: "1",
      },
    },
    result: {
      data: {
        tweets: {
          id: "1",
          content: "idが1のツイート内容です。",
        },
      },
    },
  },
];

it("「ローディング中です。。。」が表示された後に、「idが1のツイート内容です。」と画面上に表示される", async () => {
  render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <Search id={"1"} />
    </MockedProvider>
  );
  expect(
    await screen.findByText("ローディング中です。。。")
  ).toBeInTheDocument();

  expect(
    await screen.findByText("idが1のツイート内容です。")
  ).toBeInTheDocument();
});

実行結果

Apollo Clientを使用するため、ReactアプリケーションのコンポーネントツリーをApolloProviderコンポーネントで囲むと同様に、
テストでは、テストしたいコンポーネントをMockedProviderで囲みます。
MockedProviderを囲むことにより、テスト対象コンポーネント内で実行されるクエリ実行結果をモックすることができます。

例えば、
search.test.tsxでmockを以下のように定義しましたが、これはテスト対象コンポーネント内で、GET_TWEET_BY_IDをidを1に指定して、クエリを叩くとresult部分の結果が返ってくることを保証しています。つまりテストにおいて、GraphQLサーバーと通信する必要はなく、外部依存関係が削除され、テストの信頼性が向上します。

const mocks = [
  {
    request: {
      query: GET_TWEET_BY_ID,
      variables: {
        id: "1",
      },
    },
    result: {
      data: {
        tweets: {
          id: "1",
          content: "idが1のツイート内容です。",
        },
      },
    },
  },
];

ユーザーイベント + クエリを使用するコンポーネントのテスト

こちらのQiitaを参考にさせていただきました。
https://qiita.com/KNR109/items/7cf6b24bed318dab5715

テスト対象のコンポーネント

idをinput type="number"に入れ、idに対応するツイート情報を取得してくれるコンポーネントになります。

SearchForm.tsx
import { useLazyQuery } from "@apollo/client";
import React, { ChangeEvent, FormEvent, useState } from "react";
import { GET_TWEET_BY_ID } from "../gql/crud";

const SearchForm = () => {
  const [id, setId] = useState<string>("");
  const [search, { loading, error, data, called }] =
    useLazyQuery(GET_TWEET_BY_ID);
  const onSubmitSearch = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    search({
      variables: {
        id,
      },
    });
  };

  return (
    <div>
      <form
        onSubmit={(e: FormEvent<HTMLFormElement>) => {
          onSubmitSearch(e);
        }}
      >
        <input
          type="number"
          value={id}
          data-testid="input-for-search"
          onChange={(e: ChangeEvent<HTMLInputElement>) => {
            setId(e.target.value);
          }}
        />
        <button disabled={loading} data-testid="button-for-search">
          検索
        </button>
      </form>
      {called &&
        (loading ? (
          <div>ローディング中です。。。</div>
        ) : error ? (
          <div>エラーです</div>
        ) : data.tweets ? (
          <div>
            <table border={1}>
              <tbody>
                <tr>
                  <th>id</th>
                  <td>{data.tweets.id}</td>
                </tr>
                <tr>
                  <th>tweet_content</th>
                  <td>{data.tweets.content}</td>
                </tr>
              </tbody>
            </table>
          </div>
        ) : (
          <div>データがありませんでした、、、</div>
        ))}
    </div>
  );
};

export default SearchForm;

テストコード

searchForm.test.tsx
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import { MockedProvider } from "@apollo/client/testing";
import { GET_TWEET_BY_ID } from "../gql/crud";
import SearchForm from "../components/SearchForm";
import userEvent from "@testing-library/user-event";

//モックの定義
const mocks = [
  {
    request: {
      query: GET_TWEET_BY_ID,
      variables: {
        id: "1",
      },
    },
    result: {
      data: {
        tweets: {
          id: "1",
          content: "idが1のツイート内容です。",
        },
      },
    },
  },
];

it("入力フォームとボタンがリンダリングされるか", async () => {
  render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <SearchForm />
    </MockedProvider>
  );

  expect(screen.getByTestId("input-for-search")).toBeInTheDocument();
  expect(screen.getByTestId("button-for-search")).toBeInTheDocument();
});

it("入力した値が入力フォームに表示されるか、ボタンを押してLazyクエリが発火するか", async () => {
  render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <SearchForm />
    </MockedProvider>
  );
  const input = screen.getByTestId("input-for-search") as HTMLInputElement;
  userEvent.type(input, "1");
  expect(input.value).toBe("1");

  const button = screen.getByTestId("button-for-search");
  userEvent.click(button);

  expect(
    await screen.findByText("ローディング中です。。。")
  ).toBeInTheDocument();

  expect(
    await screen.findByText("idが1のツイート内容です。")
  ).toBeInTheDocument();
});

試しにscreen.debug()を追加して、レンダリングされたコンポーネントのHTMLをみてみると、正常にコンポーネントが動作していることが確認することができます。

 expect(
    await screen.findByText("idが1のツイート内容です。")
  ).toBeInTheDocument();
  
+   screen.debug();

getByTestId

https://testing-library.com/docs/queries/bytestid/

toBeInTheDocument

https://github.com/testing-library/jest-dom#tobeinthedocument

userEvent

https://testing-library.com/docs/user-event/intro#writing-tests-with-userevent

type

https://testing-library.com/docs/user-event/utility#type

click

https://testing-library.com/docs/user-event/convenience#clicks

Discussion