JestとReact Testing LibraryでTable Driven Testsを書く

Golang で テスト を書くときによく使われる Table Driven Tests を React Component のテストに使うとわかりやすくなるのではと思ったので記事を書くことにしました。

Table Driven Tests とは

Table Driven Tests は関数への入力値と期待される結果を予め配列としてとして用意しておくパターンです。
例えば Jest の Getting Started にかかれている sum 関数のテストに Table Driven Tests を使ってみるとこのようになります。

// sum.ts
export function sum(a: number, b: number) {
  return a + b;
}

// sum.test.ts
import { sum } from "./sum";

const tests = [
  {
    name: "adds 1 + 2 to equal 3",
    in1: 1,
    in2: 2,
    expect: 3,
  },
  {
    name: "adds 2 + 4 to equal 6",
    in1: 2,
    in2: 4,
    expect: 6,
  },
];

for (const t of tests) {
  test(t.name, () => {
    expect(sum(t.in1, t.in2)).toBe(t.expect);
  });
}

このような Table Driven Tests を使うと以下のメリットがあります。(個人の感想)

  • テスト用の定形コードが増えてしまうのを抑えられる
  • テストケースを増やすのが簡単
  • input に対して求められる output がわかりやすい

input に対して求められる output がわかりやすい という点が props(input)によって render される結果(output)が変化する React Component のテストにマッチするのではないかと考えました。

React Component のテストを Table Driven Tests でやってみる

以下の Modal Component のテストを考えます。
この Component は props によって、

  • closeText が変わる
  • button の表示・非表示が変わる

というシンプルな Component です。

// modal.tsx
import { PropsWithChildren } from "react";
import { createPortal } from "react-dom";

type ModalProps = {
  title: string;
  closeText?: string;
  onCancel?: () => void;
};

export const Modal = ({
  children,
  title,
  closeText = "閉じる",
  onClose,
}: PropsWithChildren<ModalProps>) => {
  if (typeof window === "undefined") return null;
  return createPortal(
    <div
      className="absolute top-0 left-0 bg-opacity-75 bg-black w-full h-full"
      onClick={onClose}
    >
      <div className="flex flex-col absolute p-4 inset-0 m-auto w-1/2 h-1/2 bg-white shadow-lg rounded-lg">
        <h2>{title}</h2>
        <div>{children}</div>
        <button
          className="bg-purple-700 text-white px-2 py-1 rounded"
          onClick={onClose}
        >
          {closeText}
        </button>
      </div>
    </div>,
    document.body
  );
};

まずは Table Driven Tests を使わずに素直に書いてみます。

// modal.test.tsx
import * as React from "react";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import { Modal } from ".";

describe("Modal", () => {
  test("render Modal component. should render button.", () => {
    render(
      <Modal
        title="title"
        onClose={() => {
          console.log("close");
        }}
      >
        <p>children</p>
      </Modal>
    );

    expect(screen.getByText("title")).toBeInTheDocument();
    expect(screen.getByRole("button")).toBeInTheDocument();
  });

  test("render Modal component. should not render button.", () => {
    render(
      <Modal title="title">
        <p>children</p>
      </Modal>
    );
    expect(screen.getByText("title")).toBeInTheDocument();
    expect(screen.queryByRole("button")).toBeNull();
  });
});

定形コードが重複してしまっていますね。
テストケースが増えてくると読みづらくなるパターンもでてきそうです。
これを Table Driven Tests で書き換えてみます。

// modal.test.tsx
import * as React from "react";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import { Modal, ModalProps } from ".";

type tests = {
  name: string;
  props: ModalProps;
  expect: () => void;
}[];

describe("Modal", () => {
  const tests: tests = [
    {
      name: "render Modal component. should render button.",
      props: {
        title: "title",
        onClose: () => {
          console.log("close");
        },
        closeText: "close",
      },
      expect: () => {
        expect(screen.getByText("title")).toBeInTheDocument();
        expect(screen.getByRole("button")).toBeInTheDocument();
      },
    },
    {
      name: "render Modal component. should not render button.",
      props: {
        title: "title",
      },
      expect: () => {
        expect(screen.getByText("title")).toBeInTheDocument();
        expect(screen.queryByRole("button")).toBeNull;
      },
    },
  ];

  for (const t of tests) {
    test(t.name, () => {
      render(
        <Modal
          title={t.props.title}
          onClose={t.props.onClose}
          closeText={t.props.closeText}
        >
          <p>children</p>
        </Modal>
      );
      t.expect();
    });
  }
});

render メソッド呼び出しのコードが 1 箇所になり先程までの発生していた重複がなくなりました。
また、与えられる props によって Component がどう変化していくのかが、テストケースを見ればわかるのでコードを読むコストが少し下がるように感じます。

※Table Driven Tests 用の型を書いているので面倒に思うかもしれませんが、ほぼ定形コードなので問題なしとします。

まとめ

  • Table Driven Tests を使うことで不要なコードの重複がなくなる
  • 与えられる props によって Component がどのように変化するのかがわかりやすくなる

感想

どうようの記事が見当たらなかったので書いてみましたが、私自身このパターンでのテストをあまり書いていないのでうまく行かないパターンがあるかもしれません。
新しい発見があれば追記しようと思います。

おまけ TypeScript + React 環境に Jest + React Testing Library を導入する

jest のセットアップ

package インストール

yarn add -D jest ts-jest @types/jest

jest の設定ファイルを作成

yarn ts-jest config:init

以下のファイルができあがる。 実行環境は jsdom にしたいので testEnvironment: "node", は削除する。

// jest.config.js
module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
};

あとは、test を書いて yarn jest でテストが実行できる。

React Testing Library の導入

package インストール

yarn add -D @testing-library/react @testing-library/jest-dom