Testing LibraryとJestを用いて親子コンポーネント間のイベント発火をテストする方法

UIコンポーネントからイベント発火したことをテストする

親子コンポーネントの連動をテストする際、子コンポーネントでイベント発火したことを親コンポーネントに伝えたか確認したいことがある。特に小コンポーネントで状態を加工して親コンポーネントに送る場合は値もテストしたい。

基本の考え方

親子間でコンポーネントのイベント発火に応じた連携を行う場合、次のような流れで動いている。

  1. 親コンポーネントからイベント発火後に呼び出す処理をpropsしておく
  2. それを小コンポーネントのイベントハンドラーにセット
  3. 小コンポーネントでイベント発火すると1が動作する

テストを書くとき問題になるのは1である。まず、親コンポーネントがないのにどうやって発火後の処理を再現すれば良いのか?と考えて詰まってしまうためである。次に3が問題となる。これはイベント発火を検知するマッチャーがわからず詰まってしまうため。順に解決できればテストを記載できる。

1はjest.fn()1というAPIを使って解決する。これはモックオブジェクト(実際の関数や依存関係を擬似的に再現するオブジェクト)2を作成するJest組み込みAPIである。つまり、親コンポーネントはないのでテスト用の関数を仮でセットしておけば良いのである。

コンポーネントのレンダリング前に、イベントハンドラーで発火する関数のモックを用意する。それをpropsに割り当てる。 実際に記載すると次のようになる。

  it("チェックボックスをクリックすると親コンポーネントへイベント発火状態を伝えること", async () => {
    // イベント検知のセットアップ
    const event = userEvent.setup();
    // イベントハンドラーのモックを用意
    const handlerMock = jest.fn();
    // propsにモックを与えて「親コンポーネントがあるように」見せかける
    const content = render(
      <AgreementCheckbox onCheckboxStatusChanged={handlerMock} />
    );
  });

jest.fnhandlerMock変数に置くのがポイントとなる。これをpropsに割り当てたり、マッチャーで用いたりするため。

イベント発火自体をテストする

テストするためにはuserEventを使い、イベントを発火させる。その後toHaveBeenCalled()3マッチャーを使い「イベントが発火したか」をテストする。検証の対象はセットアップ時に用いたjest.fn()オブジェクトとなる。

// (省略)
// チェックボックスをクリックする
await event.click(content.getByRole("checkbox"));
// イベントハンドラーが呼ばれたことを確認
expect(handlerMock).toHaveBeenCalled();

イベント発火時の引数をテストする

イベント発火時に親コンポーネントへ値を送信するとき「意図した値が送信されているか」もテストしたくなる。そのような場合toHaveBeenCalledWith()4を使う。toHaveBeenCalledWith()の引数に送信する値をセットして検証する。

// (省略)
// チェックボックス状態が親コンポーネントに送信されることを確認
expect(handlerMock).toHaveBeenCalledWith(true);

サンプルコンポーネントとテスト

コンポーネント

import React, { useState } from "react";

type AgreementCheckboxProps = {
  onCheckboxStatusChanged?: (event: boolean) => void;
};

export default function AgreementCheckbox({
  onCheckboxStatusChanged,
}: AgreementCheckboxProps) {
  const [checked, setChecked] = useState(false);

  const handleChecked = (event: React.ChangeEvent<HTMLInputElement>) => {
    setChecked(!checked);
    onCheckboxStatusChanged?.(event.target.checked);
  };

  return (
    <div>
      <input
        type="checkbox"
        name="agreement"
        onChange={handleChecked}
        checked={checked}
      />
      <label htmlFor="agreement">同意する</label>
    </div>
  );
}

テスト

  it("チェックボックスをクリックすると親コンポーネントへイベント発火状態を伝えること", async () => {
    // イベント検知のセットアップ
    const event = userEvent.setup();
    // イベントハンドラのモックを用意
    const handlerMock = jest.fn();
    // propsにモックを与えて「親コンポーネントがあるように」見せかける
    const content = render(
      <AgreementCheckbox onCheckboxStatusChanged={handlerMock} />
    );
    // チェックボックスをクリックする
    await event.click(content.getByRole("checkbox"));
    // イベントハンドラが呼ばれたことを確認
    expect(handlerMock).toHaveBeenCalled();
    // チェックボックス状態が親コンポーネントに送信されることを確認
    expect(handlerMock).toHaveBeenCalledWith(true);
  });