React Testing Library を色々試したので備忘録

色々試したので基本方針、実装上の注意点、その他ハマりどころ等を記録に残す。

基本方針

構成

  • 大まかな構成はRSpecと似た形でいけそう。
  • テストケースの構成は基本的にはRSpecのdescribe/context/itの考え方でいける。
    • ただしJestではdescribe/testしかないので、describeを入れ子にしてcontextの代用にする(わざわざcontext相当の層を作る必要はないかもしれない)
    • ここは今後の要調査ポイントの1つ
  • 各テストケース内の構成も、"データの用意" -> "処理の実行" -> "結果の検証"のRSpecでよく見る流れでいける。
    • 共通処理をメソッド化するべきか?などは今後の要調査ポイント。
    • 基本的にはベタがき & コメントで補足 の形で良いのでは?という気がしている。共通化は最小限で。

Testing Libraryについて

  • 公式サイト(https://testing-library.com/docs/intro)にある以下の記載の通り、ソフトウェアを実際に利用するようにテストことを常に認識しておく。つまりどんな機能をテストする場合も、「表示されるテキストやアイコン」で操作対象を特定し、「フォームへの入力」や「ボタン押下」を行う。

    We try to only expose methods and utilities that encourage you to write tests that closely resemble how your web pages are used.

  • ただしあくまでインテグレーションテストであるため、外部のモジュール等と連携している箇所は、モックやスタブを用いる。例えばページ遷移が発生する操作をした場合は、「ページ遷移すること」の確認ではなく、「ページ遷移を実現する(おそらく外部の)モジュールにどんなパラメータを渡したか?」をモック等を利用しつつテストすることになる

  • 公式ドキュメントが充実しているので、基本は公式ドキュメントを見れば良い。ただ次の翻訳記事も良い。 React Testing Libraryの使い方 - Qiita

実装時の注意点

"データの用意" -> "処理の実行" -> "結果の検証"のそれぞれに関して、実装時に注意する内容を記載する。

データの用意

処理の実行

  • React Testing Libraryの userEventを利用して、フォームの入力/submitやボタンのクリックを行う。
  • 操作対象となる要素の選択は React Testing Libraryのscreen.getByText() 等のクエリで取得する。
  • userEventを利用した操作はあくまで イベントの発火処理であって、htmlのDOM要素が何であれ関係はない。
    • セレクトボックスがoptionでなくWAI-ARIAの謎機能で実現されていようが、対象の要素を選択してuserEventをかませばOK。
  • フォーム送信時のパラメータを検証する等の必要がある場合は、この時点でMock Objectを、JestのMock関数を用いて作成しておく。詳細は後述。
  • あくまでインテグレーションテストなので、処理結果に外部のライブラリが挟まる場合は、そこに渡すパラメータの検証までしかできない。
  • クエリは以下参照
  • userEventは以下参照

結果の検証

  • クエリを用いるだけでも検証になる。
  • 必要に応じてexpectとカスタムマッチャを組み合わせる。
  • モックを利用して間接出力の検証を行う場合は、mockプロパティを利用する。
  • userEventの後に非同期処理が走る場合は、waitForを用いる必要もあるので注意。

ハマりどころ, 注意点

Mock Object/Test Stubの作成

一番ハマるのがここ。コンポーネントに合わせて適切なMock関数を用意する必要がある。大抵の場合、外部モジュールのモック化が必要になるので、ここではその点に絞って注意点を記載する。恐らくもっと良いやり方はあるが、ひとまず暫定出来るやり方という位置付け。

  • 基本的に以下の形でモジュールのmock化と処理の実装ができる。引数のチェックや戻り値が必要ない場合は、jest.mock('axios')だけでも可。jest.mock('XXX')でモック化したmodule内のメソッド等は、全てjest.fn()で置き換えられる。
jest.mock('axios')
axios.get.mockImplementationOnce(() =>
      Promise.resolve({ data: stocks })
)
jest.mock('axios')
const mockedAxios = axios as jest.Mocked<typeof axios>
mockedAxios.get.mockImplementationOnce(() =>
      Promise.resolve({ data: stocks })
)
  • モジュールの構成によっては
import * as reactRouterDom from 'react-router-dom'

のようにする必要もあるかもしれない。

  • 間接出力を検証するためのモック(= Mock Object)の場合は、mockプロパティを用いるために、モックの実装時にjest.fnを利用する必要がある。ただし特に戻り値等の実装をする必要がない場合は、mockImplementationOnce等を利用せずjest.mock('axios')だけでmockプロパティの利用は可能。
jest.mock('axios')
const mockedAxios = axios as jest.Mocked<typeof axios>
const myGetMock = jest.fn(() =>
      Promise.resolve({ data: stocks }))
mockedAxios.get.mockImplementationOnce(myGetMock)
  • デフォルトの実装を加えたいときは、jest.mockの第2引数を以下の形で用いる。これはあくまでデフォルトなので、別途mockImplementationOnce等で書き換え可能。
jest.mock('react-redux', () => {
  return {
    useDispatch: () => () => {
      // do nothing
    },
    useSelector: jest.fn(() => ({
      errors: [],
      stockDetail: {},
    })),
  }
})
  • mockImplementationOnce等に渡すmockの実装は、引数・戻り値の型をモック対象のメソッドに合わせないと型エラーが起きる点に注意

TypeError: window.matchMedia is not a function

というエラーが出ることがある。これはJestの問題のようだが、以下の記事を参考にすると解決できる。 reactjs - Jest test fails : TypeError: window.matchMedia is not a function - Stack Overflow