コンポーネントテストの組み立て方を整理したい

UIコンポーネントテスト、書けると複雑なコンポーネントのバグを防ぐ効果があると思っている。 しかし、慣れるまではテストケースの組み立て方法すらわからず苦戦する印象がある。

苦戦するだけならいいが、うまくいかず挫折するともったいない。組み立て方を整理してみた。

全体の流れ

全体の流れはほぼ決まっている。3から1に進むことはあり得ない。順番を覚えてしまった方が良い。

  1. 要素をレンダリングする
  2. テスト対象の要素を取得する
    • ユーザー操作がある場合はここで再現する
  3. 取得した結果が期待通りか確認する

実際のテストコードに当てはめると、次のように記載できる。

import React from "react";
import { render } from "@testing-library/react";
import Star from "./";

it("選択状態のときは赤色でレンダリングされる", () => {
  // 1. 星マークを描画するStarコンポーネントをレンダリングする
  const screen = render(<Star selected={true} />);
  // 2. redというtitle属性(ラベル付けされた星マーク)を取得
  const redStar = screen.queryByTitle("red")
  // 3. 2の結果が存在することを期待している。どうだろうか?
  expect(redStar).toBeTruthy();
});

2と3を合体させることもできる。慣れるまでは分けて記載すれば良いが、ユーザー操作が絡む場合は合体して書かないと古いレンダリング結果を参照してしまうので注意。

import React from "react";
import { render } from "@testing-library/react";
import Star from "./";

it("選択状態のときは赤色でレンダリングされる", () => {
  // 1. 星マークを描画するStarコンポーネントをレンダリングする
  const screen = render(<Star selected={true} />);
  // 2. redというtitle属性(ラベル付けされた星マーク)を取得して
  // 3. 2の結果が存在することを期待している。どうだろうか?
  expect(screen.queryByTitle("red")).toBeTruthy();
});

1ずつ確認していく。

1. 要素をレンダリングする

コンポーネントテストはブラウザではなく、Node.jsなどのJavaScript実行環境で動作する。 コンポーネントがどうなっているかを確かめるため、まずはDOMツリー(≒コンポーネントのHTML構造)を専用ライブラリで再現する必要がある。

レンダリング方法はテストに利用しているライブラリに依存する。Vue Test UtilsならmountshallowMountだし、Testing Libraryならrenderを使う。 チュートリアルやExample(サンプル)に必ず記載があるので真似すれば良い。オプションは困ったら調べれば良いので、まずは「レンダリングを行う」ことだけ考える。

propsの設定方法もライブラリに依存する。この辺りも必ずチュートリアルがあるので真似して設定する。

it("選択状態のときは赤色でレンダリングされる", () => {
  // 1. 星マークを描画するStarコンポーネントをレンダリングする
  const screen = render(<Star selected={true} />);
});

2. テスト対象の要素を取得する

どの要素をテストするか?をここで決める。慣れていないと色々クエリがあって迷ってしまう。 ここはライブラリによって異なるため、調べる前に「何を取得したいか」を整理しておく。

具体的には、対象はテキスト?HTMLタグ?コンポーネント…???という風に要素の中身を可能な限り分解しておく。 そして、それを文章化して1つずつ調べる。

import React from "react";
import { faStar } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

interface StarProps {
  selected?: boolean; // 選択中かどうか
}

export default function Star({
  selected = false,
  ...props
}: StarProps): JSX.Element {
  return (
    // FontAwesomeIconライブラリを使ってSVGを描画
      <FontAwesomeIcon
        icon={faStar} // アイコンは星マーク
        color={selected ? "red" : "grey"} // 色はここで決める
        title={selected ? "red" : "grey"} // title属性を付与
      />
  );
}

星マークのコンポーネントの例であれば、次のように整理できる。

  • FontAwesomeIconライブラリを使ってSVGをレンダリングしている。SVGにはtitle属性がついている
  • 「red」というtitle属性が存在する
  • ライブラリはコンポーネントになっており「FontAwesomeIcon」という名前である

整理した結果とテストに利用しているライブラリが得意なことを比較し、うまいこと取得できないか考える。ここが一番苦戦する。 ライブラリによって設計思想が異なるのと、得意・不得意がはっきりしているため。理解できるまでは毎回調べ回る必要があり、時間がとてもかかる。

例えばVue Test Utilsは内部実装もテストできるように設計されており、findComponentというAPIがあり、コンポーネントを探すことができる。一方、Testing Libraryはユーザー操作に近しい状態でテストする設計になっている。内部実装はテストできず、アクセシビリティを考慮した取得用APIを使う。

ではどうやって探せば良いか?一昔前はGitHubやStackOverflowで探していた。個人のブログや技術ブログサービスも参考になる。ルーティングライブラリなどは実装例のサンプルがGitHubリポジトリに存在することが多く、ここからテスト例を探すことも可能。

最近はChatGPTなどAIツールに聞いてみるとサンプルが出てくる。ドキュメントで裏どりしてから使うと良い。

2’. ユーザー操作を再現する

ボタンをクリックした・フォーム入力した、などはこのタイミングで再現する。非同期の概念が出てくるので一気に難易度が跳ね上がる。最初はこういうところは飛ばしていくのが良い。 挫折して諦めるより全然良いのだ。

3. 取得した結果が期待通りか確認する

expectなど、テストライブラリのアサーション関数を使って期待通りか確認する。ここはJest・Vitest・jsdomなどのAPIを調べていくことになる。 最初に「取得した要素はどうなっていて欲しいか」を言語化しておくと路頭に迷いにくい。

慣れるまでは情報量が一番多いJestを使うと良い。書籍も大体Jestで書かれている。そのうち変わるかもしれないが、迷ったらたくさんサンプルが出てくるテストライブラリを使ってテストするのが良い。 初回は実装サンプルを見るか、AIの力で書いてもらい、ドキュメントを確認する。次は自力で書いてみるの繰り返しで身につけると良い。

まとめ

  • コンポーネントテストは全体の流れが決まっているので覚えれば迷わない
  • まずはレンダリングする
  • 次に何をテストするか言語化する
  • 最後にどうなっていればいいか言語化する
  • 言語化した内容の書き方を調べる

コンポーネントテストを書こうとすると、複雑なセットアップをするものを避けたくなる。結果的に設計の改善につながる。デグレードも回避しつつ設計も改善できるんだからいいことづくめてある。 悪いのは「テスト対象の要素を取得する」ハードルが高いことか。本当にわからないときは全然わからない。一旦諦めて後で再チャレンジするとうまくいくこともある。

参考URL