LoginSignup
448
494

More than 1 year has passed since last update.

【入門】フロントエンドのテスト手法まとめ

Last updated at Posted at 2022-07-19

はじめに

自分は2021年に新卒でweb系の開発会社にフロントエンジニアとして入社し2022年で2年目になります。

実務ではReact×TypeScriptを利用したフロント周りの開発をメインで行なっていなす。

今回は実務でNext.jsプロジェクトにテストを導入することになり「React-Testing-Library」と「Jest」について改めて学び直したのでその内容を紹介します。

はじめに「React-Testing-Library」と「Jest」の概要を説明しその上で具体的なテストコードを何パターンか書いていきます。

この記事の対象者

  • フロントエンドのテストの概要を知りたい人
  • React-Testing-LibraryとJestについて知りたい人
  • 具体的なテストの書き方を学びたい人

なお本記事では、React-Testing-Libraryの具体的な書き方についてをメインにしているので「そもそもテストとは何か」「なぜテストを書くのか」といったテスト自体の詳しい解説は簡単に留めています。

なぜテストを書くのか

テストを書く主な目的は、

  • 開発効率と開発体験を上げる
  • 開発製品の品質の担保
  • 画面に不具合が発生し、ユーザーの期待通りの挙動にならないことを防ぐ

具体的にはテストを導入することで、

  • 開発中に早い段階でエラーを発見しやすくし開発コストを減らすことができる
  • 機能が増えた時に、既存の機能を壊さない可能性を上げる
  • ユーザーの操作が期待通りかをチェックできる

といった恩恵を得ることができます。

フロントエンドテストの種類

フロントエンドのテスト手法は様々ありますが、今回はReact-testing-libraryの著者であるKent C. Dodds氏のTesting Trophyを参考に紹介していきます。

なおTesting Trophyについての詳しい解説はKent C. Dodds氏の下記の記事を読んでみてください。

image.png

Testing TrophyではTrophyの上のレイヤーに行くほど

  • テストの速度は遅くなる
  • 実装コストは高くなる
  • テストの信頼性が高くなる

といったことが挙げられています。

ここでは簡単にTesting Trophy紹介されている4種類のテストの内容を説明します。

  • Static Test (静的テスト)
  • Unit Test (単体テスト)
  • Integration Test (結合テスト)
  • End to End Test (E2Eテスト)

Static Test (静的テスト)

静的テストとはコードのタイプミス(タイポ)や型エラーをチェックするテストです。FlowやTypeScriptなどの静的型解析を導入することでチェックすることができます。

Unit Test (単体テスト)

単体テストは個々の独立した関数やコンポーネントが動作するかをチェックするテストです。

単体テストのテストツールとしてはJestが一番人気のフレームワークになっています。

またReactコンポーネント指向においては「コンポーネントが担う責務を明確にする」という単一原則というものがあります。

この考えに基づいたコンポーネント設計(良いコンポーネント設計)をすることでユニットテストを書くことができます。

Reactのコンポーネント指向の考え方については、下記のの記事でわかりやすく解説されています。

Integration Test (結合テスト)

結合テストは各コンポーネントや関数を組み合わせた時に機能が問題なく動作するかをチェックするテストです。

結合テストのテストツールとしては、JestReact Testing Libraryがあります。

End to End Test (E2Eテスト)

E2EテストはサーバーのAPIやブラウザ等の環境でアプリケーションを動かし、システム全体が正しく動くかをチェックするテストです。

E2Eのテストツールとしては、CypressPuppeteerが挙げられます。

本記事で紹介するテストについて

本記事紹介するテストツールはReactの公式サイトでも推奨されているJestReact-testing-Libraryを使っていきます。

また今回紹介するテストは、

  • 単体コンポーネントのテスト
  • ブラウザでユーザーがアプリを操作する挙動のテスト
  • コンポーネント間のデータの受け渡しテスト
  • Reactフックのテスト
  • mockAPIを使ったテスト

をコードを書きながら解説をしていきます。

Next.jsでの環境構築

今回はNext.js×TypeScriptのプロジェクトにテストを導入します。

Next.js×TypeScirptのプロジェクトを作成

任意にプロジェクトディレクトリを作成し、以下のコマンドを実行します。

このコマンドによってNext.js×TypeScriptの環境を構築します。

  npx create-next-app . --typescript --use-npm

React-testing- libraryのインストールと設定

作成したプロジェクト内で以下のコマンドを実行します

npm i -D jest @testing-library/react @types/jest @testing-library/jest-dom @testing-library/dom babel-jest @testing-library/user-event jest-css-modules

babel設定ファイルにNext.jsプロジェクトにJestでテストをすることを明示

プロジェクトのルートで.babelrcを作成します。

  touch .babelrc 

作成したファイルに下記を追加します。

.babel
    {
        "presets": ["next/babel"]
    }

package.jsonの設定

次にTestの対象として除外するファイルとCSSモジュールの読み込みを防ぐ記述をpackage.json書いてきます。

package.json
  "jest": {
    "testPathIgnorePatterns": [
      "<rootDir>/.next/",
      "<rootDir>/node_modules/"
    ],
    "moduleNameMapper": {
      "\\.(css)$": "<rootDir>/node_modules/jest-css-modules"
    }
  }

また同様にコマンドでテストが実行できるようにpackage.jsonscriptに以下を追加

package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "test": "jest --env=jsdom --verbose", // これを追加した
    "lint": "next lint"
  },

--env=jsdom --verboseのオプションを付与することでテストファイル1個1個に対してテストが通ったかを確認することができる。

これで環境構築は完了したので次の章からテストコードを書いてテストを実行していきます。

レンダリングテスト

コンポーネントのレンダリングテスト

最初はテスト対象のコンポーネントがレンダリングされているかをテストします。

まずはpages配下にあるindex.tsxの記述を以下のように書き換えます。

pages/index.tsx
import type { NextPage } from "next";

const Home: NextPage = () => {
  return <div>Hello World</div>;
};
export default Home;

この状態で以下のコマンドを叩いてlocalhost:3000にアクセスすると「Hello World」と表示されていることが確認できます。

npm run dev

次に作成したHomeコンポーネントがレンダリングされ「Hello World」と実際に表示されるかをテストするコードを書いていきます。

まずルート配下に__tests__ディレクトリを作成しその中にHome.test.tsxファイルを作成します。

__tests__/Home.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import Home from "../pages/index";

describe("rendering", () => {
  it("Should render Hello text", () => {
    render(<Home />);
    expect(screen.getByText("Hello World")).toBeInTheDocument();
  });
});
  • describeは関連するテストをブロック単位でまとめるメソット
    • 第1引数にテストブロックの説明
    • 第2引数にテストケースを記述するコールバック関数
  • itは実際のテストを記述する
    • 第1引数はテストの説明
    • 第2引数はテスト内容を記述するコールバック関数
  • render()は引数に指定したJSXをレンダリングする
  • expectはテスト結果を評価するメソット

ちなにみexpect内のメソットは種類が豊富なので下記の公式ドキュメントを参考にしてみてください。

今回のテストコードはHomeコンポーネントをレンダリングし画面内に「Hello World」という文字が表示されるかをテストしています。

pages/index.tsx
import type { NextPage } from "next";

const Home: NextPage = () => {
  return <div>Hello World</div>;
};
export default Home;

実際に下記のコマンドでテストを実行すると

npm run test

テストがパス(成功)していることがわかります。
スクリーンショット 2022-07-02 14.33.37.jpg

次にテストファイルを「Hello Suzuki」に変更してテストを実行してみます。

__tests__/Home.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import Home from "../pages/index";

describe("rendering", () => {
  it("Should render hello text", () => {
    render(<Home />);
    expect(screen.getByText("Hello Suzuki")).toBeInTheDocument();
  });
});

該当のHomeコンポーネントには「Hello Suzuki」は表示されないのでテストは失敗します。

スクリーンショット 2022-07-02 14.35.42.jpg

次はレンダリング後に対象の要素が存在するかをテストしていきます。

検索フォームのレンダリングテスト

対象の検索フォームは以下の構成で成り立っています。

  • 検索ワードを入れる入力フォーム
  • 検索クエリを送信するボタン

レンダリング時に入力フォーム(Input)と送信ボタン(Button)が表示されているかをテストします。

検索フォームコンポーネントの作成

まずはルート配下にcomponentsディレクトリを作成しSearchForm.tsxを作成します。

※ 今回テストを目的としているためスタイルは当てていません。

components/SearchForm.tsx
import React, { useState } from "react";

export const SearchForm = () => {
    // 入力フォームの値を管理するステート
  const [value, setValue] = useState<string>("");
  // 入力フォームの値を変更する関数
  const onchange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };
   // 検索ボタンをクリックした際に発火する関数
  const onClick = () => {
    // 検索の処理
  };
  return (
    <div>
      <input type="text" onChange={onchange} value={value} />
      <button onClick={onClick}>検索</button>
    </div>
  );
};

SearchFormコンポーネントについて

  • valueは入力フォームに入れた値を管理するstate
  • onchangeで入力した内容にstateを更新
  • onClickは検索ボタンをクリックした際に発火する(一旦ダミー)

検索フォームコンポーネントのテストコードを書いていく

テスト内容としてはレンダリング時に、

  • 入力フォーム(input)が表示されているか
  • ボタン(button)が表示されているか

をテストします。

__tests__/SearchForm.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import SearchForm from "../pages/index";

describe("rendering", () => {
  it("Should render SearchForm", () => {
    render(<SearchForm />);
    expect(screen.getByRole("textbox")).toBeTruthy();
    expect(screen.getByRole("button")).toBeTruthy();
  });
});

前半は最初に書いたテストとほとんど同じです。

  • getByRoleは指定されたロールをもつ要素を検索します。

なおHTMLタグの各ロールについては下記に掲載されています。

該当ファイルをテストするコマンドで対象のテストを実行

 npm run test SearchForm.test.tsx

同様にこちらも成功することがわかります。

スクリーンショット 2022-07-02 14.56.34.jpg

今はinputbuttonといったHTMLタグのロールをテストの中で直接指定していましたが、IDを付与してテストをすることもできます。

IDを付与してテストをするために検索フォームを以下のように書き換えます。

components/SearchForm.tsx
  return (
    <div>
      <input
        data-testid="search-input"
        type="text"
        onChange={onchange}
        value={value}
      />
      <button data-testid="search-button" onClick={onClick}>
        検索
      </button>
    </div>
  );

入力フォームにsearch-input、検索ボタンにsearch-buttonというidを付与しました。

テストファイルは以下のように書き換えます。

__tests__/SearchForm.test.tsx
describe("rendering", () => {
  it("Should render SearchForm", () => {
    render(<SearchForm />);
    expect(screen.getByTestId("search-input")).toBeTruthy();
    expect(screen.getByTestId("search-button")).toBeTruthy();
  });
});
  • getByTestIdで該当のテストIDをもつ要素を取得することができます

こちらも同様にテストを実行するとパスすることが確認できます。

 npm run test SearchForm.test.tsx

レンダリングテストではレンダリング時に該当のコンポーネントの要素が正しく表示されるかをテストしました。

ユーザーイベントのテスト

次にユーザーが実際にクリックしたり入力したりするユーザーイベントのテストをしていきます。

入力フォームへの入力テスト

まずは検索フォームにおいて、ユーザーが入力した値が入力フォームに入ってくるかをテストします。

テスト対象は先ほどと同じくSearchFormコンポーネントを利用します。

components/SearchForm.tsx
import React, { useState } from "react";

export const SearchForm = () => {
  const [value, setValue] = useState<string>("");
  const onchange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  const onClick = () => {
    // 検索の処理
  };
  return (
    <div>
      <input type="text" onChange={onchange} value={value} />
      <button onClick={onClick}>検索</button>
    </div>
  );
};

入力フォームのテストコードを書いていく

入力フォームのテストは以下の手順で行います

  • 入力フォームの要素を取得する
  • 取得した入力フォームにユーザーイベントで値を入れる
  • 入力フォームの値がユーザーイベントで入れた値になっているかをテストする

上記のテストコードで書いていきます。

SearchForm.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import userEvent from "@testing-library/user-event";

import SearchForm from "../pages/index";

describe("input form onChange event", () => {
  it("input test", () => {
    render(<SearchForm />);
    // HTMLElementに型推論されているのでasで型アサーション
    const inputValue = screen.getByRole("textbox") as HTMLInputElement;
    // ユーザーが入力フォームに"test"と入力する動作をテスト
    userEvent.type(inputValue, "test");
    // 入力フォームの値が"test"になっているかをチェック
    expect(inputValue.value).toBe("test");
  });
});

詳しく解説をしていきます。

  • 先ほどと同様にgetByRoleで入力フォームの要素を取得
    • asで型アサーションしています
  • userEventで入力のテストをする
    • userEvent.typeでユーザーのタイピングイベントを実行
    • 第1引数にイベントを行う要素の取得したInput要素を格納
    • 第2引数に入力する値である"test"という文字を格納
  • expectで入力された値がinputに入っているかをテスト

テストを実行するとパスするのがわかります

npm run test SearchForm.test.tsx

次に、SearchFormコンポーネントのInputフォームの入力値が変えられないようにvalueを固定値に書き換えて、テストが失敗するかを確認してみます。

components/SearchForm.tsx
  return (
    <div>
      // valueを固定値に変更する
      <input type="text" onChange={onchange} value={"変わりません"} />
      <button onClick={onClick}>検索</button>
    </div>
  );

ローカル環境を立ち上げて確認すると、入力フォームに値を入力しても固定値のため値が変わらないようになっています。

スクリーンショット 2022-07-02 15.51.27.jpg

実際にテストを実行してみると失敗します。

npm run test SearchForm.test.tsx

スクリーンショット 2022-07-02 15.52.34.jpg

ボタンをクリックイベントのテスト

次にユーザーが検索フォームの検索ボタンをクリックした時の挙動をテストします。

検索ボタンがクリックされたタイミングで親コンポーネントから渡ってきた関数(onSubmit)を発火させるようにsearchFormコンポーネントの内容を書き換えます。

components/SearchForm.tsx
import React, { useState } from "react";

type SearchFormProps = {
  onSubmit: (value: string) => void; 
};

export const SearchForm: React.VFC<SearchFormProps> = ({ onSubmit }) => {
  const [value, setValue] = useState<string>("");
  const onchange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };
  
  // 入力フォームに値がある場合はonSbmitを発火し、そうでない場合はconsoleを表示
  const onClick = () => {
    if (value) {
      onSubmit(value);
    } else {
      console.log("入力フォームが空です");
    }
  };
  return (
    <div>
      <input type="text" onChange={onchange} value={value} />
      <button onClick={onClick}>検索</button>
    </div>
  );
};

また親コンポーネントはsearchFormコンポーネントに受け渡すonSbmit関数を作成しpropsで受け渡せるように書き換えます。

pages/index.tsx
import type { NextPage } from "next";
// components
import { SearchForm } from "../components/SearchForm";

const Home: NextPage = () => {
  // SearchFormコンポーネントに渡す関数
  const onSubmit = (value: string) => {
    console.log(value);
  };
  return (
    <div>
      <SearchForm onSubmit={onSubmit} />
    </div>
  );
};

export default Home;

ボタンをクリックイベントのテストコードを書いていく

挙動としては以下のようになります

  • ユーザーがボタンをクリックする
  • 入力フォームが空の場合はpropsで渡ってきたonSbmitは発火しない
  • 入力フォームが空でなかった場合は親から渡ってきたonSbmitが発火する

まずはじめに入力フォームが空の場合に親から渡ってきたonSbmitが発火しないテストを書いてみます。

SearchForm.test.tsx
import { SearchForm } from "../components/SearchForm";

describe("input form onChange event", () => {
  it("ボタンクリック", () => {
    // propsで受け渡す用のモック関数をテスト用に作成
    const onSubmit = jest.fn();
    render(<SearchForm onSubmit={onSubmit} />);
    // ユーザーのクリックをテスト
    userEvent.click(screen.getByRole("button"));
    expect(onSubmit).not.toHaveBeenCalled();
  });
});
  • 親から受け渡す関数をモックとして作成する(jest.fn())
  • 作成したmock関数をpropsとして渡す
  • ユーザーがボタンをクリックするテストを書く
  • クリック時に親から渡ってきたonSbmitが発火しないことをテスト(not.toHaveBeenCalled())

not.toHaveBeenCalled()についての詳しい解説はJestの公式ドキュメントでも見れます。

テストを実行します

 npm run test SearchForm.test.tsx

SearchFormコンポーネント内の条件分岐によって親から渡ってきたonSbmitは発火せずにconsoleが出力されていることが確認できます。

スクリーンショット 2022-07-03 17.56.23.jpg

次にユーザーが入力フォームに値を入力した場合にボタンをクリックしたときの挙動をテストします。

この場合は親から渡ってきたonSbmitが発火されます。

SearchForm.test.tsx
describe("input form onChange event", () => {
  it("should trigger output function", () => {
    // propsで受け渡す用のモック関数をテスト用に作成
    const onSubmit = jest.fn();
    render(<SearchForm onSubmit={onSubmit} />);
    const inputValue = screen.getByRole("textbox");
    userEvent.type(inputValue, "test");
    userEvent.click(screen.getByRole("button"));
    expect(onSubmit).toBeCalled;
  });
});

テストを実行します

 npm run test SearchForm.test.tsx

スクリーンショット 2022-07-03 18.04.42.jpg

以上でユーザーイベントのテストについての解説は終了です。

次に親コンポーネントから渡ってきたpropsのデータ受け取り周りについてのテストを見ていきます。

propsでのデータ受け取りのテスト

次にpropsで配列型のデータが渡ってきてそれをmapメソットのループで表示するテストをします。

Cardsコンポーネントの作成

まずは、propsで受け渡された配列をループで表示するためのCardsコンポーネントを作成します。

Cardコンポーネントは親コンポーネントからオブジェクト形式のユーザー情報の配列を受け取り、mapメソットで受け取った情報を全件表示するコンポーネントです。

components/Cards.tsx
import React from "react";

type User = {
  id: number;
  name: string;
};

type CardsProps = {
  userInfos: User[];
};

export const Cards: React.VFC<CardsProps> = ({ userInfos }) => {
  return (
    <>
      {userInfos.length === 0 ? (
        <p>ユーザー情報は0です</p>
      ) : (
        <ul>
          {userInfos.map((userInfo) => (
            <li key={userInfo.id}>
              id:{userInfo.id} name:{userInfo.name}
            </li>
          ))}
        </ul>
      )}
    </>
  );
};

※ mapの中はCardコンポーネントとして切り出した方がよいが今回は直書きしています。

親コンポーネントはCardsコンポーネントに対して配列が渡せるように書き換えます。

pages/index.tsx
import type { NextPage } from "next";
// components
import { Cards } from "../components/Cards";

const userInfos = [
  { id: 1, name: "Tom" },
  {
    id: 2,
    name: "Mary",
  },
  {
    id: 3,
    name: "Bob",
  },
];

const Home: NextPage = () => {
  return <Cards userInfos={userInfos} />;
};

export default Home;

Cardsコンポーネントのテストコードを書いていく

これによって該当のカードコンポーネントと配列を受け渡す親コンポーネントの準備が整いました。

下記の内容をテストしていきます。

  • 配列の中身が空だった場合のテスト
  • 配列の中身が存在する場合のテスト

まずは配列の中身が空だった場合のテストを書いていきます。

__tests__/Cards.test.tsx
import { getByText, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import { Cards } from "../components/Cards";

describe("cards component props pass test", () => {
  it("Testing for missing array elements", () => {
    // propsの配列データの中身が存在しないとき
    render(<Cards userInfos={[]} />);
    expect(screen.getByText("ユーザー情報は0です")).toBeInTheDocument();
  });
});

propsの配列の中身が0の場合はCardsコンポーネントで「ユーザー情報は0です」と表示されるかをテスト。

npm run test Cards.test.tsx

こちらはテストが通ることが確認できます。

スクリーンショット 2022-07-03 19.30.19.jpg

次に配列の中身が存在する場合に渡ってきたユーザー情報の配列が表示されるかをテストします。

少し長くなるのでコードを分けて解説をしていきます。

まずpropsで受け渡すユーザー情報の配列のダミーデータを用意します。

渡ってきたユーザー情報の配列はmapでループされてliの中で表示されるのでli要素を取得する処理を記述します。

__tests__/Cards.test.tsx
import { getByText, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import { Cards } from "../components/Cards";
import { userInfo } from "os";

describe("cards component props pass test", () => {
  it("Testing for the presence of array elements", () => {
    // Cardsコンポーネントに渡すダミーデータ
    const dummyUserInfos = [
      { id: 1, name: "tom" },
      {
        id: 2,
        name: "mary",
      },
      {
        id: 3,
        name: "bob",
      },
    ];
    render(<Cards userInfos={dummyUserInfos} />);
    // li要素を全件取得し配列に格納
    const userInfos = screen.getAllByRole("listitem");
    // 配列に格納された最初のli要素を取得しtextを表示する
    console.log(userInfos[0].textContent);
  });
});

データが取れているかを確認するためにテストコマンドを実行すると、consoleの値が表示されているのが確認できます。

スクリーンショット 2022-07-03 19.54.57.jpg

次に以下の手順に沿ってテストコードを追記していきます

  • 取得したuserInfostextContentを配列に格納する
  • 受け渡したダミーデータをループで回しliで表示される形式にする
  • 上記の2つの配列を比較し一致しているかをテスト
__tests__/Cards.test.tsx
  it("Testing for the presence of array elements", () => {
    // Cardsコンポーネントに渡すダミーデータ
    const dummyUserInfos = [
      { id: 1, name: "Tom" },
      {
        id: 2,
        name: "Mary",
      },
      {
        id: 3,
        name: "Bob",
      },
    ];
    render(<Cards userInfos={dummyUserInfos} />);
    // liで表示される文言を取得し配列に格納
    const userInfos = screen
      .getAllByRole("listitem")
      .map((item) => item.textContent);
       // ダミーデータをliで表示される文言と形式を合わせる
    const dummyItems = dummyUserInfos.map(
      (item) => `id:${item.id} name:${item.name}`
    );
       // 上記の2つを比較し一致しているかをテスト
    expect(userInfos).toEqual(dummyItems);
  });

これによってpropsで受け渡されたダミーデータが実際にCardsコンポーネント内で表示されているデータと一致していることを確認できます。

Reactフックのテスト

次にReactフックのuseEffectのテストを紹介します。

今回は下記の構成でuseEffectを利用します。

  • ブログ用のデータを外部API(JSONPlaceholder)から取得するfetch関数を作成
  • 作成した関数をuseEffectでレンダリング時に呼び出す
  • 外部APIから取得したデータをstateへ格納
  • 取得したデータをBlogページ内で表示させる

下記の内容をテストしていくコードを書いていきます。

  • API通信中(dataがない時)はデータがBlogページ内には「ローディング中」と表示される
  • API通信完了後は取得したデータが表示されている

まず新しくBlogページを作成します。

pages/blog.tsxというファイルを作成します。

blog.tsx
import type { NextPage } from "next";
// components

const BlogPage: NextPage = () => {
  return <p>ブログです</p>;
};
export default BlogPage;

作成したら「localhost:3000/blog」にアクセスすすると以下の表示がされています。

スクリーンショット 2022-07-09 15.26.28.jpg

次に実際に外部APIからデータを取得するfetch関数を作成し、レンダリング時に呼び出します。

少し処理が長いのでコメントアウトをつけています。

pages/blog.tsx
import axios from "axios";
import type { NextPage } from "next";
import { useEffect, useState } from "react";

// 取得するブログ記事の型
type Post = {
  userId: number;
  id: number;
  title: string;
  body: string;
};

const BlogPage: NextPage = () => {
  // 取得したブログデータを格納するstate
  const [postDate, setPostDate] = useState<Post>();
  // 外部APIからブログデータを取得
  const getPost = async (): Promise<Post> => {
    const response = await axios.get(
      "https://jsonplaceholder.typicode.com/posts/1"
    );
    return response.data;
  };
  // レンダリング時にAPIコール関数を実行し取得したデータでstateを更新
  useEffect(() => {
    try {
      const getDate = async () => {
        const result = await getPost();
        setPostDate(result);
      };
      getDate();
    } catch (e) {
      console.log(e);
    }
  }, []);

  return (
    <div>
      {!postDate ? (
        <p>ローディング中</p>
      ) : (
        <p>
          記事ID{postDate.id}:{postDate.title}
        </p>
      )}
    </div>
  );
};

export default BlogPage;
  • getPostでaxisoを使ってJSONPlaceholderからデータを取得します
  • useEffect内でgetPostを呼び出し取得したデータをstateに格納します
  • JSX内では条件分岐でデータがない場合は「ローディング」、ある場合はデータを表示させます

これによってレンダリング時にAPIコール後にIDが1の記事が表示されるのを確認できます。

実際にテストコードを書いていきます。

最初にAPIコール中は「ローディング中」という文字が表示されるテストを書きます。

__tests__/Blog.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import BlogPage from "../pages/blog";

describe("useEffect rendering", () => {
  it("Should render fetch method before data is retrieved", async () => {
    render(<BlogPage />);
    expect(screen.getByText("ローディング中")).toBeInTheDocument();
  });
});

テストを実行します。

npm run test Blog.test.tsx

スクリーンショット 2022-07-09 16.12.23.jpg

次にAPIコール終了後データが表示されているかを確認します。

__tests__/Blog.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import BlogPage from "../pages/blog";

describe("useEffect rendering", () => {
  it("Should render fetch method before data is retrieved", async () => {
    render(<BlogPage />);
    expect(screen.getByText("ローディング中")).toBeInTheDocument();
  });
  it("Should render fetch method after data is retrieved", async () => {
    render(<BlogPage />);
    expect(await screen.findByText(/記事ID/)).toBeInTheDocument();
  });
});

実際にテストが通っていることを確認できます。

APIの挙動をMockしテストする

React-testing-libraryの公式ドキュメントでも推奨されている「Mock Service Worker*」を使います。

まずはmswをプロジェクトにインストールします。

npm i msw --save-dev

ユーザー情報を表示するコンポーネントを作成

  • JSONPlaceholderuser情報を取得するgetUserを作成
  • 取得前は「データはありません」という文言を表示
  • 取得に成功した場合はuser情報を表示する
  • 取得に失敗した場合はerrorを表示する
  • なおgetUserbuttonをクリックした時に発火するようにする
pages/user.tsx
import axios from "axios";
import type { NextPage } from "next";
import { useEffect, useState } from "react";

// 取得するブログ記事の型
type User = {
  id: number;
  name: string;
  username: string;
  email: string;
};

const UserPage: NextPage = () => {
  // 取得したブログデータを格納するstate
  const [user, setUser] = useState<User>();
  const [error, setError] = useState<string>("");
  // 外部APIからブログデータを取得
  const getUser = async () => {
    try {
      const response = await axios.get(
        "https://jsonplaceholder.typicode.com/users/1"
      );
      // 必要な情報だけ抽出する
      const userInfo = {
        id: response.data.id,
        name: response.data.name,
        username: response.data.username,
        email: response.data.email,
      };
      setUser(userInfo);
    } catch (e) {
      setError("Request failed ");
    }
  };

  return (
    <div>
      {!user && !error && (
        <>
          <p>データはありません</p>
          <button onClick={getUser}>ユーザー情報を取得</button>
        </>
      )}
      {user && <h3>名前: {user.name}</h3>}
      {error && <p data-testid="error">{error}</p>}
    </div>
  );
};

export default UserPage;

実際にボタンをクリックする前は下記のように表示されています。

スクリーンショット 2022-07-16 10.15.00.jpg

ボタンをクリックすると下記のようになります。

スクリーンショット 2022-07-16 10.15.04.jpg

取得するエンドポイントをhttps://jsonplaceholder.typicode.com/users/0に変更すると、存在しないuser情報なのでエラーが表示されます。

スクリーンショット 2022-07-16 10.16.28.jpg

APIのMockテストを書いていく

まずはじめに先ほどインストールしたmswのsetupServerを利用してモックのサーバーを作成します。

  • エンドポイントはhttps://jsonplaceholder.typicode.com/users/1
  • 成功したらstatusCodeが200番とユーザーの情報が返ってくる
__tests__/User.test.tsx
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { rest } from "msw";
import { setupServer } from "msw/node";

// APIサーバーを作成する
const server = setupServer(
  rest.get("https://jsonplaceholder.typicode.com/users/1", (_, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({
        id: 1,
        name: "Leanne Graham dummy",
        username: "Bret dummy",
        email: "Sincere@april.biz.dummy",
      })
    );
  })
);

これでモックの擬似的なサーバーを作成することができました。

サーバーの準備をする

次に作成したサーバーの起動、リセット、終了の設定をしていきます。

  • beforeAllでファイルが読み込まれた際にサーバーを起動する
  • afterEachで各テストケースが終わるたびにリセットする
  • afterAllで処理終了後にサーバーを停止する
__tests__/User.test.tsx
beforeAll(() => server.listen());
afterEach(() => {
  server.resetHandlers();
});
afterAll(() => server.close());

テストケースを書いていく

テストするコンポーネントのJSXは以下のようになっているので、

__tests__/User.test.tsx
    <div>
      {!user && !error && (
        <>
          <p>データはありません</p>
          <button onClick={getUser}>ユーザー情報を取得</button>
        </>
      )}
      {user && <h3>名前: {user.name}</h3>}
      {error && <p data-testid="error">{error}</p>}
    </div>

下記の内容をテストするコードを書いていきます。

  • 成功した場合はh3タグに「名前: Leanne Graham dummy」が表示される
  • 失敗した場合はerrorというテストIDにエラー文が表示される
  • 失敗した場合はh3タグは表示されない

成功した場合

  • button要素をクリックしてAPIをコール
  • findByRoleで非同期処理をしh3タグのテキストを取得(後で詳細解説)
  • 取得した値がAPIのレスポンス値と一致しているかをtoEqualでテスト
__tests__/User.test.tsx
describe("mocking API", () => {
  it("Fetch success Should display fetched data correctly", async () => {
    render(<UserPage />);
    userEvent.click(screen.getByRole("button"));
    expect((await screen.findByRole("heading")).textContent).toEqual(
      "名前: Leanne Graham dummy"
    );
  });
});

ここで今までに登場したqueryの使い方を公式ドキュメントを元にまとめます。

query 使い方
getBy 特定のクエリに一致する要素を取得する
queryBy 特定のクエリに一致する要素が存在しないことを取得
findBy 特定のクエリに一致する要素を非同期で取得する

実際にテストを実行するとpassしていることが確認できます。

 npm run test User.test.tsx

スクリーンショット 2022-07-17 7.53.32.jpg

失敗した場合

次にAPIコールが失敗した場合のテストを書いてきます。

__tests__/User.test.tsx
  it("Fetch failure Should display error message", async () => {
    // error用のサーバーを作成
    server.use(
      rest.get(
        "https://jsonplaceholder.typicode.com/users/1",
        (_, res, ctx) => {
          return res(ctx.status(404));
        }
      )
    );
    render(<UserPage />);
    userEvent.click(screen.getByRole("button"));
    expect((await screen.findByTestId("error")).textContent).toEqual(
      "Request failed"
    );
    expect(screen.queryByRole("heading")).toBeNull();
  });
  • server.useで先ほど作成したサーバーのエラー用レスポンスを作成
  • findByTestIdでerrorが表示される要素を取得
  • toEqualで表示されるエラー文と比較
  • queryByRoleで成功時に表示されるh3タグを取得しnullであることをチェック

実際にテストを実行するとpassしていることが確認できます。

 npm run test User.test.tsx

スクリーンショット 2022-07-17 8.00.15.jpg

最後に

448
494
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
448
494