UIコンポーネントテストで要素を取得する

UIコンポーネントテストで鬼門となるのは「テスト対象の要素を取得する」ことである。 要素が取得できないとそもそもテストしようがないのである。UIコンポーネントパーツはお決まりのパターンがあるので真似れば良いが、100%参考にできるわけでもない。また、UIコンポーネント同士を連動させた結合テストはアプリケーション仕様に依存するため、パターン化して当てはめることは不可能。ある程度は考え方を理解した上で、自分で記載できた方が良い。

今回テストに使うコンポーネントはChangeIconコンポーネントである。実装は次のようになっている。

import React, { useState } from "react";
import { faMusic, faImage, faTent } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

export default function ChangeIcon() {
  const [icon, setIcon] = useState({ value: faMusic, text: "音楽" });

  const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const value = e.target.value;
    if (value === "music") {
      setIcon({ value: faMusic, text: "音楽" });
    } else if (value === "image") {
      setIcon({ value: faImage, text: "画像" });
    } else if (value === "tent") {
      setIcon({ value: faTent, text: "テント" });
    }
  };

  return (
    <section>
      <h2>アイコンを選ぶ</h2>
      <select onChange={handleChange}>
        <option value="music">音楽</option>
        <option value="image">画像</option>
        <option value="tent">テント</option>
      </select>
      <h2>選択中のアイコン</h2>
      <span>選択しているアイコンは{icon.text}です</span>
      <FontAwesomeIcon icon={icon.value} data-testid="icon" />
    </section>
  );
}

事前準備をしておく

仕様のTODOリストが整理されているかを再チェックする。要素の取得はUIコンポーネントテストの中でもトライ&エラーしながら進めなければならない。よって、仕様が整理できていないと慌ててしまい、どんどん本来テストしたかったものから遠ざかってしまう。一度に考慮する量を減らし、精神的な負担を減らして集中できるようにする。

ChangeIconコンポーネントのTODOリストは存在しない。書き始める前に整理しておく。次の内容はサンプルである。

### ページのタイトルテキスト

- h2タグで実装されている
- タイトルは「アイコンを選ぶ」

### プルダウン

- プルダウン(ドロップダウン = 選択肢)を選ぶとアイコンが切り替わる
- アイコンが切り替わると「選択しているアイコンは〇〇です」と表示される
- 〇〇はアイコン名が入る。アイコン名はプルダウンの選択肢と一緒
- 初期状態ではアイコンとテキストは表示されない。選択肢は「選択してください」になっている

クエリを使いわける

テストを書き始める前にテストフレームワークのクエリを確認すると良い。要素の取得方法はテストフレームワークによって考え方が異なるため、サンプルや特徴を押さえてから始めると試行錯誤せずにすむ。

例えば、Testing Libraryであれば「ユーザーの操作内容を再現するように」という思想がある。よって、使えるクエリもその思想を反映している。ドキュメントを探すときは「Query」や「Queries」で検索すると良い。Testing Libraryのクエリに関するドキュメントは「About Queries1」に記載がある。

繰り返すときはAllを使う

UIコンポーネントの中に複数回同じ要素が出てくる場合、クエリにAllをつけたものを使う。ここでの「要素」とはテキストやHTMLタグのことである。 理由は一番始めに出てくる要素を取得して意図通りテストできないか、「複数の要素は取得できない」扱いとなりテストが失敗してしまうから。

例えば、「h2タグが2つ存在することをテストしたい」とする。Testing Libraryで次のようなテストを書いてしまうと、「TestingLibraryElementError: Found multiple elements with the role "heading"」エラーになりテストが失敗する。

it("h2タグが存在する", () => {
  const content = render(<ChangeIcon />);
  // h2タグが2つ存在することを確認しているつもり
  expect(content.getByRole("heading", { level: 2 })).toHaveLength(2);
});

ChangeIconコンポーネントにh2タグは2つ存在する。getAllByRole()を使って取得しなければならない。修正すると次のようになる。

it("h2タグが2つ存在する", () => {
  const content = render(<ChangeIcon />);
  // h2タグが2つ存在することを確認
  expect(content.getAllByRole("heading", { level: 2 })).toHaveLength(2);
});

data-testidを使う

どうしても要素が取得できない・取得方法がわからないときがある。その場合は要素にdata-testidのようなテスト用要素を付与する。 Testing LibraryではgetByTestId2というクエリがある。data-testid要素に付与してテストすれば良い。

クエリ方法がわからないからとテストを諦めるより、data-testidを付与してテストする方がよっぽど良いと思う。

<!-- コンポーネントの実装。対象要素だけ抜粋。data-testidを付与しておく -->
<FontAwesomeIcon icon={icon.value} data-testid="icon" />
it("アイコン要素が存在することを確認する", () => {
  const content = render(<ChangeIcon />);
  // アイコン要素が存在することを確認
  expect(content.getByTestId("icon")).toBeInTheDocument();
});