初期状態(props)を活用して仕様を表現する

ここでの初期状態とはUIコンポーネントテストを書くとき設定するpropsのことである。 propsが複数ある場合、全てのパターンを網羅しようとすると組み合わせの数次第ではテスト数が膨大になる。 すると、テストケースの維持やメンテナンスが難しくなる。

自動テストの導入は品質向上のきっかけとしたり、変更時のデグレードを防ぐために行なっている。それにも関わらず、テストケースのメンテナンスに悩殺されていれば意味がない。 ほどほどに、しかし必要なテストは書いておきたい。どのように初期状態を設定すれば良いだろうか。

TODOリストを元にパターンを洗い出す

TODOリストで先に仕様を整理しているため、それを元に取りうるpropsパターンを全て洗い出す。最初から過不足なく書こうとすると手が止まってしまうため、まずはパターンを洗い出す。パターンの出し方は積の法則の考え方を使って出すとわかりやすい。『数学Ⅰ・A 基礎問題精講(四丁増補版、2018, p168)』1では、積の法則は次のように定義されている。

2つの事象A, Bの起こり方がそれぞれp通り, q通りあるとき, A, Bがともに起こる場合の数は p × q通り

propsに当てはめて考えてみる。例えば、propsにcolorcountを設定できるStarRatingコンポーネントがあったとする。

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

type StarRatingProps = {
  color: "red" | "blue" | "yellow";
  count: number;
};

export default function StarRating({
  color = "red",
  count = 3,
}: StarRatingProps): JSX.Element {
  return (
    <>
      {[...Array(count)].map((n, i) => (
        <FontAwesomeIcon key={i} icon={faStar} color={color} />
      ))}
      <p>{`星の数は${count}個です`}</p>
    </>
  );
}

StarRatingは星の数でレート(評価)を表現するUIコンポーネントである。colorは星の色を設定できるpropsである。redblueyellowの3種類が設定できる。一方、countはレートの数を管理している。星1つから星5つをつけることができる。また、countの値を使って「星の数はいくつか」を表示するテキストも表示している。

Ratingコンポーネントが取りうる状態はいくつだろうか。積の法則を元に考えると、props colorのパターン × props countのパターン × props count のテキストを表示するパターン = 3 × 5 × 5 = 75パターンとなる。

重複しているパターンを取り除く

パターンを洗い出すことはできたが、全ての場合の数をテストするのは大変である。先に述べたRatingコンポーネントだけでも75ケース存在する。propsが1つ追加されると積の法則でどんどん増えてしまう。こうなると要件の追加・変更時に修正範囲が膨大となり、テストが機能しなくなってしまう。

そこで、すでにテスト済みのものは取り除けないか考える。整理したTODOと比較し、「propsの値に応じて動きが変わる組み合わせはないか」を探す。次に、すでに存在するパターンは取り除くようにする。

Ratingコンポーネントの例では「propsの値に応じて動きが変わる組み合わせ」は存在しない。colorredのときに必ず星三つをつけるようなルールは存在しないためである。よって、テストケースは和の法則で考えれば良くなる。props colorのパターン + props countのパターン + props count のテキストを表示するパターン = 3 + 5 + 5 = 13パターンとなる。

さらに考えると、countは1回テストしてしまえば数が変わったときもcountの数だけ星を表示する点では同じである。count=5のときは表示しない、などの特殊な仕様があるならテストケースに入れるべきだが、そうではない。よってcountは最低1回テストすれば良い。テキスト表示の場合も同じ考え方が利用できる。

結果的にテストケースは次の5パターンまで絞れる。

  1. color=redのとき星の色が赤色で設定されている
  2. color=blueのとき星の色が青色で設定されている
  3. color=yellowのとき星の色が黄色で設定されている
  4. countの数だけ星が描画されている
  5. 星の数を示すテキストがcountの値を使って描画されている

重複しているパターンはUIコンポーネントテストの範囲だけでなく、ビジュアルリグレッションテスト2やE2Eテスト3など、他の自動テストでカバーできるパターンはテストしなくても良い。不具合が多く出てくる部分は重複していてもテストケースに入れておく。壊れて複数回直すよりマシだからである。

この例だとcolorの値と設定される色がおかしくなっていないかは見た目 = ビジュアルリグレッションテストの領域が得意なものである。1から3は省略するか、colorの設定が有効に働くかだけをUIコンポーネントテストで確かめる。もしビジュアルリグレッションテストがなければ1から4まで省略せずに記載するか、代わりの手段を考える。

今回はcolorのテストを取り除く方向で考える。テストケースは次のようになる。

  1. countの数だけ星が描画されている
  2. 星の数を示すテキストがcountの値を使って描画されている

パターン : テスト = 1 : 1になるようにpropsを設定する

重複しているパターンを除外できれば、あとはテストケースの雛形を作りテストを書くだけである。 デフォルト値が設定されている場合は変えた方が良い。実はデフォルト値しか設定できませんでした、みたいなことがあり得るため。

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

it("countの数だけ星が描画されている", () => {
  // デフォルト値は3なので変更した値を渡す
  const content = render(<StarRating color="red" count={5} />);
  // 星の要素を取得
  const stars = content.getAllByTitle("star");
  // 要素数は5個
  expect(stars).toHaveLength(5);
});

it("星の数を示すテキストがcountの値を使って描画されている", () => {
  // デフォルト値は3なので変更した値を渡す
  const content = render(<StarRating color="red" count={4} />);
  // 意図通りのテキストになっているか確認
  expect(content.getByText("星の数は4個です")).toBeTruthy();
});