UIコンポーネントテストでユーザーイベントを再現するときのポイント

UIコンポーネントテストでは「ボタンをクリックするとテキストが表示されることを確かめたい」など、ユーザーの操作を再現したい場面が出てくる。 ユーザー操作の種類は無限にあるように見えるが、考え方を身につけていればUIコンポーネントテストを書ける。 細かな文法はテストフレームワークごとに異なるが、考え方は変わらない。

ユーザー操作をイベントリファレンスと紐づける

MDNの中に『イベントリファレンス』1というページがある。マウス操作など、ブラウザ上でユーザーが行う操作を「イベント」という。 このイベントはEvent2インターフェイスとして定義されており、JavaScriptで取得できる。 つまり、ユーザー操作の取得方法がわからない場合は「イベントリファレンス」を参照するとよい。

テストフレームワークのドキュメントはイベント名がわかっている前提で書かれている。そのためどのイベントを取得すれば良いか分からない状態でドキュメントを参照してしまうと挫折しやすい。 通常は実装を組み立てる前や既存コードにテストを追加するタイミングでイベント名を把握している。しかし、ジュニアメンバーやボタンクリックのときはonClickv-on:clickを使えば良い、という浅い理解度のときはイベント名の把握は難しい。よって、まず再現したい操作のイベントを分かった状態で調べると良い。

要素の参照を再利用しない

ユーザーイベントを再現する場合、イベントの再現してテストを実行することが多い。 すると、同じ要素をconstなどにおいて参照を作りたくなるが、避けた方が良い。 具体例は次のコードのimageの部分である。

it("ドロップダウンの値を変更するとアイコンが変わる", async () => {
  const event = userEvent.setup();
  const content = render(<ChangeIcon />);
  // アイコン要素の参照を取得
  const image = content.getByTestId("icon");

  // data-icon属性がimageになっていることを確認
  expect(image).toHaveAttribute("data-icon", "music");

  // ドロップダウンの値を変更
  await event.selectOptions(content.getByRole("combobox"), "image");

  // data-icon属性がimageになっていることを確認
  expect(image).toHaveAttribute("data-icon", "image");
});

このように要素の参照を作ってテストしてしまうと、テスト結果が不安定になってしまう。理由は「イベント発火後にレンダリング結果を反映していない」場合、正しく実装できているのにテストが落ちてしまう可能性があるため。先ほどのテストケースに当てはめるとコードベースでは選択肢をimageに変更できているが、expectimageの参照が初期状態のままなのでテストが落ちる場合がある。

法則性があるわけではなく、テストの実行時の環境や状態に依存する。テストがかなり不安定になってしまう。

it("ドロップダウンの値を変更するとアイコンが変わる", async () => {
  const event = userEvent.setup();
  const content = render(<ChangeIcon />);
  // アイコン要素の参照を取得 = musicの状態
  const image = content.getByTestId("icon");

  // data-icon属性がimageになっていることを確認
  expect(image).toHaveAttribute("data-icon", "music");

  // ドロップダウンの値を変更
  await event.selectOptions(content.getByRole("combobox"), "image");

  // コードベースではimageに変更できているが、expect`image`の参照が初期状態のままなのでテストが落ちる
  expect(image).toHaveAttribute("data-icon", "image");
});

面倒でも毎回expect対象はクエリして取得した方が良い。常に今のレンダリング結果を取得するため、テストの安定性が保証される。

it("ドロップダウンの値を変更するとアイコンが変わる", async () => {
  const event = userEvent.setup();
  const content = render(<ChangeIcon />);

  // アイコン要素はmusicであること
  expect(content.getByTestId("icon")).toHaveAttribute("data-icon", "music");

  // ドロップダウンの値を変更
  await event.selectOptions(content.getByRole("combobox"), "image");

  // imageになっていることを確認
  expect(content.getByTestId("icon")).toHaveAttribute("data-icon", "image");
});