Open7

React x TypeScript テスト導入とJestの基本

TooToo

Reactコンポーネントのテストを書こうのインプット

英語にはなってしまいますが、クエリはこちら、アサーションはこちら、ユーザーイベントはこちらに公式ドキュメントによる詳細な説明があります

testing-libraryをインストールする

次のコマンドを実行してtesting-libraryをインストール

yarn add \
  @testing-library/react@14 \
  @testing-library/jest-dom@5 \
  @testing-library/user-event@14

package.json

{
  "name": "component-test-tutorial",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "5",
    "@testing-library/react": "14",
    "@testing-library/user-event": "14",
    "@types/jest": "^27.0.1",
    "@types/node": "^16.7.13",
    "@types/react": "^18.0.0",
    "@types/react-dom": "^18.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "typescript": "^4.4.2",
    "web-vitals": "^2.1.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

index.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import { SimpleButton } from "./SimpleButton";
 
const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <SimpleButton />
  </React.StrictMode>
);

SimpleButton.tsx

import { useState } from "react";
 
export const SimpleButton: () => JSX.Element = () => {
  const [state, setState] = useState(false);
  const handleClick = () => {
    setState((prevState) => !prevState);
  };
  return <button onClick={handleClick}>{state ? "ON" : "OFF"}</button>;
};

SimpleButton.test.tsx

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SimpleButton } from "./SimpleButton";

test("ボタンをクリックするとON/OFFの表示が切り替わる", async () => {
  // ここにテストの中身を書いていきます
  const user = userEvent.setup();
  render(<SimpleButton />);
  const simpleButton = screen.getByRole("button");
  expect(simpleButton).toHaveTextContent("OFF");
  await user.click(simpleButton);
  expect(simpleButton).toHaveTextContent("ON");
});

テストを実行する際はyarn test

TooToo

フロントエンドテスト

JestとTesting Libraryは役割が違うという話

公式
アールエフェクト様 記事

Testing Libarary

  • React用のReact Testing Libraryが提供されておりcreate-react-appを利用してReactプロジェクトを作成すると自動でインストールされる
  • Tesing Libraryはテストを実行したいコンポーネントの描写やクリックイベントの実行、描写した内容からの要素の取得等に利用される

Jest

  • JavaScriptのTesting Framework/Test Runner
  • テストのファイルを自動で探し出し、テストを実行、テストを実行した結果期待通りの正しい値を持っているか関数(matchers)を利用してチェックを行い、テストが成功か失敗かの判断を行う
TooToo

Jestのターミナルの見方

以下はnpm testを実行した結果

Jestのテスト結果に表示される各項目の意味は以下

  • Test Suites: テストスイートは、関連するテストの集まりです。一般的には、一つのファイルに記述されたテスト群を指します。この項目は、実行されたテストスイートの数と、そのうち成功した数、失敗した数を示します。
  • Tests: これは個々のテストケースの数を示します。テストケースは、特定の機能が期待通りに動作するかを検証するためのコードです。この項目は、実行されたテストケースの総数と、そのうち成功した数、失敗した数を表示します。
  • Snapshots: スナップショットテストは、コンポーネントのレンダリング結果をキャプチャし、以前のレンダリングと比較することで変更を検出するテストです。この項目は、使用されたスナップショットの総数、更新された数、失敗した数を示します。
  • Time: この項目は、テストの実行にかかった総時間を示します。また、推定された実行時間も表示されることがあります。これは、テストのパフォーマンスを把握するのに役立ちます。
    これらの項目は、テストの実行結果を概観するのに役立ち、どのテストが成功し、どのテストが失敗したか、またテストの実行にどれくらいの時間がかかったかなど、重要な情報を提供します。
TooToo

各型毎の簡単なmatchersのテスト方法

ドキュメント

// toBe
it("数値のテスト", () => {
  expect(2 + 2).toBe(4);
});

it("文字列のテスト", () => {
  expect("Jest").toBe("Jest");
});

it("Booleanのテスト", () => {
  expect(true).toBe(true);
});

// toEqual
it("配列のテスト", () => {
  const arr1 = [1, 2, 3];
  const arr2 = [1, 2, 3];
  expect(arr1).toEqual(arr2);
});

it("オブジェクトのテスト", () => {
  const obj1 = {
    a: 1,
    b: 2,
  };
  const obj2 = {
    a: 1,
    b: 2,
  };
  expect(obj1).toEqual(obj2);
});

// not
it("2+2は5ではない", () => {
  expect(2 + 2).not.toBe(5);
});
TooToo

配列をチェックする実際のテストを書いてみる

つまりポイント
テストケースでエラー分を書くときは、tsファイルで書いたthrow new Error(`アイテム: ${item} は存在しません`);の形式に合わせて、testケースでも "アイテム: banana は存在しません"と書く必要があるという事

test.ts

/**
 * 【演習】
 *  1. `addItem`メソッドが、アイテムをリストに追加できることを確認するテストケース
 *  2. `removeItem`メソッドが、アイテムをリストから削除できることを確認するテストケース
 *  3. `removeItem`メソッドが、存在しないアイテムの削除を試みたときにエラーをスローすることを確認するテストケース
 */

export class ShoppingList {
  public list: string[];

  constructor() {
    this.list = [];
  }

  addItem(item: string): void {
    this.list.push(item);
  }

  removeItem(item: string): void {
    const index = this.list.indexOf(item);
    if (index === -1) {
      throw new Error(`アイテム: ${item} は存在しません`);
    }
    this.list.splice(index, 1);
  }
}

test.ts

import { ShoppingList } from "./test";

describe("ShoppingListClassのテスト", () => {
  let shoppingList: ShoppingList;

  beforeEach(() => {
    shoppingList = new ShoppingList();
  });

  describe("addItem", () => {
    it("listにstiringの配列を増やす", () => {
      shoppingList.addItem("element");
      expect(shoppingList.list).toEqual(["element"]);
    });
  });

  describe("removeItem", () => {
    it("listから指定したindexの配列を削除します", () => {
      shoppingList.addItem("apple");
      shoppingList.removeItem("apple");
      expect(shoppingList.list).not.toContain("apple");
    });

    it("リストにアイテムが存在しない場合はエラー", () => {
      expect(() => shoppingList.removeItem("banana")).toThrow(
        "アイテム: banana は存在しません"
      );
    });
  });
});