Testing Libraryでrender関数を使ってUIコンポーネントをレンダリングする方法は複数ある

テストランナーを動かす前に「UIコンポーネントは今どういう状態なのか」をセットアップしなければいけない。UIコンポーネントが必須のpropsを持つ場合、propsを与えなければUIコンポーネントは動作しない。また、 特定の場面で挙動が変わる場合は特定の場面自体を再現してテストしたい。

コンポーネントの初期状態の設定方法はいくつかある。

UIコンポーネントを直接インポートする

Testing Libraryではrender関数の引数にUIコンポーネントを与えることでレンダリングできる。Reactコード内で小コンポーネントとしてコンポーネントを描画する要領でpropsを設定してやれば良い。

一度変数に取り出してレンダリングする方法

具体的には次のようなコードになる。propsはコンポーネントに直接割り当てる。

it("h1ヘッダーテキストが存在すること", () => {
  // render関数を使ってAboutコンポーネントをレンダリング
  const content = render(<About />);
  // レンダリングされたコンポーネントの中にテキストが存在するか確認
  expect(content.getByText("About")).toBeInTheDocument();
});

contentに一度取り出すことで、複数回要素を取得する場合はcontentにクエリ用APIを書くだけで済む。 ただし、一回しかアクセスしないような場合は記述が冗長になってしまう。簡潔に書きたい場合は別の手段を取ると良い。

自分は基本的には変数に取り出す方法でテストを書いている。テストを動かす所まで漕ぎ着けるのが重要と考えているためである。 また、ジュニアメンバーが多い環境では「変数にクエリをつければ要素が取得できる」ことが伝わりやすい。少しでもトリッキーな実装が来るとテストをメンテナンスできない状態になりやすいため、高度な知識なしでできる方法を採用している。

デストラクチャリングを使ってクエリ対象のAPIだけ取り出す方法

次のようにrender関数内から値を取り出す際、分割代入(デストラクチャリング1を使う方法がある。 クエリに使うAPIを分割代入で取り出しておくことで、次の行では直接アクセスできる。

必要なAPIだけを使ってクエリする必要がないため、テストの記載が簡潔になる。

it("h1ヘッダーテキストが存在すること", () => {
  // デストラクチャリングを使いrender関数で取得したオブジェクトからgetByTextメソッドを取得する
  const { getByText } = render(<About />);
  // 取得してあるのでgetByTextが直接使える
  expect(getByText("About")).toBeInTheDocument();
});

テスト内で複数要素が必要だと、都度render関数を呼び出すことになる。テストが簡潔に書ける、という利点が失われるためcontentのように変数に取り出す方が良い。また、要素がイベント発火によって状態を変えたことをテストする場合、古い参照を見ているせいでテストが失敗することがある。

余計な切り分けが発生すると焦ってしまうため、可能な限り失敗しにくいように初期描画のコードを書いた方が良い。Testing Libraryのドキュメントでも、render関数にはコンポーネントを引数に渡している。

単純かつコンテンツへのアクセスが一回で済む場合、分割代入でテストを書くと良い。今回の例ではAboutというテキストにアクセスしているだけなので、分割代入が最適なように感じる。

screen APIを使ってレンダリングする方法

screen2 APIを使うとDOMの状態に直接アクセスできる。変数にrender関数の結果を取り出さなくてもテストが書けるので、取りかかりやすい。また、Testing Libraryの公式ドキュメントではscreenを使う想定でサンプルが書かれている。 使い初めはこの方法が取りかかりやすいかもしれない。

// あらかじめscreen APIをインポートしておく
import { render, screen } from "@testing-library/react";

it("h1ヘッダーテキストが存在すること", () => {
  // render関数を使ってAboutコンポーネントをレンダリング
  render(<About />);
  // screen APIを使ってDOMにアクセスし、テキストが存在するか確認
  expect(screen.getByText("About")).toBeInTheDocument();
});

注意点としては、screenはグローバル変数的にDOMにアクセスする可能性がある点。screenはグローバルな状態、つまり同じテストファイル内でのscreenを利用すると全て同じコンポーネントの状態を参照する。

つまり、途中で操作をして戻し忘れると後続のテストに影響を及ぼす

例えば、英語ロケールに変えた状態で次のテストを追記するとき、意図していないのにずっと英語ロケールのコンポーネントを参照してしまう。

it('ロケールを英語にする', async () => {
  const event = userEvent.setup();
  render(<About />);
  // ロケールを英語にする
  const button = screen.getByRole('button', { name: "ロケール変更" });
  // これ以降はずっと英語ロケールの状態でscreenがレンダリングされている
  userEvent.click(button);

  // テキストが存在していることを確かめる
  expect(screen.getByText("Type Your Name")).toBeInTheDocument();
});

it('名前を入力する欄のタイトルテキストが存在すること', () => {
  render(<About />);
  // 英語ロケール状態でテストが始まるため、並び順次第で意図通り動作しない
  expect(screen.getByText("名前を入力してください")).toBeInTheDocument();
});

これを防ぐには、状態を変えるテストは最後の方で実施すれば良い。しかし考えるのは大変だし見落としてしまう。プロダクション用コードでは基本的にscreenを利用せず、Testing Libraryの利用に慣れるため・どうしても必要なとき以外は避けた方が良い。

変数のスコープを短く切ってミスしないように工夫した方が良いよ、というのと同じである。