Reactコンポーネントのテストツールとして主流なReact Testing Libraryに用意されているクエリ(メソッド)の公式ドキュメントを全文翻訳しポイントをまとめました。
要素の選択にどのクエリを使うか分からなくて困るワン。
読みにくいテストコードは後で見返すのが疲れる。
クエリの使い方を押さえると効果的にTesting Libraryを使えるよ
React Testing Libraryのドキュメントは親切ですが、特徴や設計思想を知らないといピンとこないところもあります。
Testing Libraryの勘所をピンポイントで理解したい人は、特にクエリ優先度の項目を読むと良いと思います。
豊富な補足とともに意訳していくので、一緒に読んでいきましょう。
React Testing Libraryのクエリとは、ページ上の要素を見つけるために提供されているメソッド群です。クエリにはget, find, query
キーワードで区別される3種類の異なるタイプがあり、それらの違いは以下の2点です。
- 要素を発見できなかった時に、エラーを発生させるかどうか。
- クエリ(メソッド)の返り値としてPromiseを返したり、retry(再試行)を行ったりするかどうか。
ページ上のどんな要素を選択しようとしているかによって、適切なクエリは異なります。ページ上の要素に最もアクセスしやすいようなセマンティックなクエリの利用法に関しては、クエリ優先度ガイドをご覧ください。
セマンティッククエリとは、WAI-ARIAに準拠した要素選択を行えるクエリです。
セマンティック(Semantic)には英語で、「形式に捉われない、意味論の」といった意味があります。
RTLでは、ページ上の要素を考える時、要素を実装しているHTML要素が何かということに捉われず、その要素がユーザーにとってどんな役割を果たすかを中心にテストしたいと考えています。
ページ上のHTML要素のユーザーにとっての役割を決めている基準の一つが、WAI-ARIAです。RTLでは、WAI-ARIAで定められた役割(HTMLのrole)で要素選択するクエリを標準として提供し、セマンティッククエリと呼称しているのです。
要素を選択したら、イベントAPIまたはuser-eventライブラリを利用してユーザーイベントを発生させ、ユーザーインタラクションをシミュレートできます。加えて、Jestとjest-domライブラリで要素の挙動に対する例外を設定して、テストを行います。
Testing Libraryのクエリと協調して動作するヘルパーメソッドがあります。
要素の表示・非表示は、ユーザーアクションへの応答して(非同期的に)行われることがあります。このような場合、waitForクエリやfindByクエリといった非同期APIを使用して、await
でDOM要素の変化を(同期的に)待つことができます。
特定の要素の(直下の)子要素を指定したい時には、withinが利用できます。
また、各クエリはオプションをつけて呼び出すことができ、必要に応じて再試行までのタイムアウトやデフォルトのテストIDを設定できます。
- 1つの要素を選択するクエリ
getBy...
: クエリに一致したDOMノードを返します。もし一致した要素が無かった場合または複数見つかった場合は、エラーメッセージを返します。(複数要素を取得されることを期待するテストでは、getAllBy
を代わりに使用してください。)queryBy...
: クエリに一致したDOMノードを返します。もし一致した要素が無かった場合、null
を返します。返り値のnull
は、指定されたパターンに一致する要素が無いことをテストしたいときに有用です。また複数の要素が見つかった場合にはエラーを返します。(複数要素が取得されることを期待するテストでは、queryAllBy
を代わりに使用してください。)findBy...
: クエリに一致するDOMノードがあるかどうかについてのPromise
を返します。findBy
のデフォルトのタイムアウトは1000ミリ秒です。(デフォルトでは)1000ミリ秒経過後に、指定されたパターンに一致する要素が無かった場合または複数の要素が見つかった場合、Promise
はreject
で解決されます。(複数要素が取得されることを期待するテストでは、findAllBy
を代わりに使用してください。)
- 複数の要素を選択するクエリ
getAllBy...
: クエリに一致したDOMノードの配列を返します。もし一致した要素が1つも無かった場合、エラーメッセージを返します。queryAllBy...
: クエリに一致したDOMノードの配列を返します。もし一致した要素が無かった場合、空配列[]
を返します。findAllBy...
: クエリに一致するDOMノードが(複数個も含めて)あるかどうかについてのPromise
を返します。findAllBy
のデフォルトのタイムアウトは1000ミリ秒です。(デフォルトでは)1000ミリ秒経過後に、指定されたパターンに一致する要素が1つも無かった場合にPromise
はreject
で解決されます。findBy
メソッドはgetBy*クエリとwaitFor
メソッドの組み合わせです。findBy
メソッドは、最後の引数としてwaitFor
のオプションを受け取ることができます。
使用例:await screen.findByText('text', queryOptions, waitForOptions)
概要表
クエリの種類 | 0個マッチ | 1個マッチ | 2個以上マッチ | Retry(Async/Await) |
---|---|---|---|---|
1つの要素の検索 | ||||
getBy... | エラー | 一致した要素 | エラー | 無し |
queryBy... | null | 一致した要素 | エラー | 無し |
findBy... | エラー | 一致した要素 | エラー | 有り |
複数要素の検索 | ||||
getAllBy... | エラー | 一致要素の配列 | 一致要素の配列 | 無し |
queryAllBy... | 空配列[] | 一致要素の配列 | 一致要素の配列 | 無し |
findAllBy... | エラー | 一致要素の配列 | 一致要素の配列 | 有り |
React Testing Libraryの基本理念に基づいて、テストはページやコンポーネントに対するユーザーのインタラクションを可能な限り模擬したものでなくてはなりません。この観点から、私たちは使用すべきクエリに以下の優先順位を定めています。
- あらゆる人が利用可能なクエリ:読み上げ機能などの支援技術を利用する視覚障がい者も含めた、全てのユーザーに共通した体験を反映するクエリ
-
getByRole
このメソッドは、アクセシビリティツリーに現れる全ての要素を選択できます。name
オプションを使用すれば、アクセシブル名で要素を絞り込みできます。
このクエリは、最も使用優先度が高いクエリです。getByRole
で取得できない要素はあまりありません(もし取得できないのであれば、アクセシビリティの観点からUIの実装に不備がある恐れがあります)。
name
オプションを使用した典型的な使用例です:getByRole('button', {name: /submit/i})
。getByRole
に指定するrole
属性はMDNの対応表を確認してください。
-
getByLabelText
このメソッドは、フォーム内の要素を選択するのに適しています。ユーザーがwebフォームを操作するとき、ユーザーは要素のラベルテキストで入力フィールドを見つけます。
getByLabelText
はこの振る舞いを模倣しますので、フォーム内の要素をテストしたい時このメソッドは最優先されるべきです。 -
getByPlaceHolderText
前提として、プレースホルダーはラベルの代わりにはなりません。しかしながら、プレースホルダー以外のものが利用できない場合に限りこのメソッドを利用してください。 -
getByText
フォームの外では、ユーザーが要素を見つける主要な手がかりはテキストコンテンツです。このメソッドは、div
,span
,p
要素などのユーザーインタラクションの無い要素を選択するのに適しています。 -
getByDisplayValue
フォーム要素のvalueの値を指定して要素を指定するメソッドです。
-
- セマンティッククエリ:上記以外で、HTML5またはARIA準拠のセレクターで要素を指定します。
-
getByAltText
img
,area,
input
要素などにalt
属性が指定されている場合、alt
属性をこのメソッドで指定して要素の選択に利用できます。 -
getByTitle
title
属性で要素を選択できるメソッドです。しかし、title
属性はスクリーンリーダーで必ずしもサポートされておらず、視覚障害のないユーザーにもデフォルトで表示されないことに留意してください。
-
- (開発者が設定した)テストIDによる検索クエリ
-
getByTestId
HTML要素のrole
属性やテキストで要素の選択ができず、開発者が設定したカスタム属性(data-*
の形の属性など)が唯一の手段のとき、このメソッドを利用してください。例えば、テキストが動的に変化するページの場合です。
-
DOMテストを行うライブラリで利用できる基本的なクエリには、第一引数として、(コンポーネントをラップするための)コンテナ要素を渡す必要があります。
ただし、Testing Libraryに含まれるほとんどのフレームワークは、単にレンダーするだけで、コンポーネントをコンテナ要素でラップする仕様になっています。つまり、コンポーネントのレンダー時にマニュアル的にコンテナ要素を作成し、クエリに渡す必要はありません。
加えて、document.body
を指定したいときには、下記の使用例のようにscreen
を利用できます。
クエリの主な引数は文字列型、正規表現、または関数です。また、DOM要素のノードを解析する方法を調整するためのオプションも利用できます。詳細はTextMatchの項目を参照してください。
React, Vue, Angularのようなフレームワーク、または(単なる)HTMLコードでレンダーできる、以下のようなDOM要素が与えられたとします。
所定の要素を検索するために、以下のようにクエリを利用できます(今回は、byLabelText
を利用しました)。
javascriptのオブジェクトの形で、クエリにオプションを渡すことができます。クエリの種類に応じた利用可能なオプションは、各クエリのドキュメントを参照してください。例えば、byRole APIのドキュメントなどです。
DOMテストを行うライブラリ群に含まれる全てのクエリは、第一引数に(コンポーネントをラップするための)コンテナ要素を取ります。
document.body
全体の指定はあまりにありふれているので、DOMテストを行うライブラリはクエリ群の他に、document.body
に事前にバインドされたscreen
オブジェクトをexport
しています。
screen
オブジェクトには全てのメソッドを使用することができます。
下記がscreen
の使用例です。
screen
を利用するためには、グローバルなDOM環境が必要です。
Jestを使用してテストしている場合、testEnvironment変数をjsdom
に設定してください。
もし、HTMLのscript
タグも含めてテストを実施したい場合、scriptタグがHTMLのbody
タグの直後にあることを確実にしてください。実例はここにあります。
ほとんどのAPIは、TextMatch
を引数として取ることができます。TextMatch
とは文字列型、正規表現、またはシグネチャ関数のいずれかです。
例:(content?: string, element?: Element | null) => boolean
型で、一致要素があればtrue
を、なければfalse
を返す。
次のHTMLが与えられたとします。
上記のdiv
要素にマッチするコード:
div
要素にマッチしないコード:
TextMatch
を引数としてとるクエリ群は、最後の引数として、テキストマッチングの精度に影響するオプションをオブジェクトの形で受け取ります。
exact
:デフォルト値はtrue
です。文字列全体への完全一致をテストし、大文字小文字を区別します。falseに設定された時は、部分一致をテストし、大文字小文字を区別しないようになります。- 正規表現または関数引数とともに使用されるとき、設定は無効化されます。
- 多くの場合、
{ exact: false }
のようにマニュアル的にexact
値をfalse
に設定するよりも、正規表現を用いた方がより柔軟なあいまい検索を行うことができるため、正規表現を用いた設定が好まれます。
normalizer
:デフォルトのテキスト正規化の挙動を上書きするためのオプションです。デフォルトの挙動などについては正規化の項目を参照してください。
DOM要素のテキストにマッチングを行う前に、DOMテストを行うライブラリは自動的に正規化を行います。デフォルトでは、先頭と末尾にあるスペースを文字列から削除し、文字列中の2つ以上連続したスペースは1つに変換します。
もしデフォルトの正規化を行わないか、代わりに別の正規化(例えば、Unicode制御文字の削除)を行いたい場合、オプションとしてnormalizer
関数を指定することができます。normalizer
関数は、文字列を引数にとり、正規化された文字列を返すような関数です。
デフォルトの正規化を呼び出すgetDefaultNormalizer
メソッドがあります。
デフォルトの正規化を上書きしたい時、normalizer
に指定する関数を0から書くこともできますが、getDefaultNormalizer
で呼び出した関数の一部を上書きして作ることもできます。
getDefaultNormalizer
は、オブジェクトの形でオプションを引数に取り、オプションにより以下の正規化をするかどうか選択できます。
trim
:デフォルト値はtrue
です。文字列の先頭と末尾のスペースを削除します。collapseWhitespace
:デフォルト値はtrue
です。文字列中の連続したスペースを1つに変換します。
先頭と末尾のスペースを削除しない文字列マッチを行うコード:
次の使用例は、以下のような正規化を行うコードです:
- 先頭と末尾のスペースを削除しない
- Unicode制御文字を削除する
- その他の挙動は、デフォルトの正規化と同様にする
Testing Libraryが提供するクエリに加えて、要素の選択に通常のquerySelector DOM APIを用いることもできます。
ただし、class
やid
による要素の選択は非推奨であり、(他の方法で選択できない場合の)避難ハッチであることを留意してください。class
やid
はユーザーからは見えないからです。
必要であればtestid
を使用し、セマンティックでないクエリを仕方なく使用している意図を明確にすることで、HTML 要素とクエリの間に安定的な取り決めを確立するべきです。
Testing Libraryのクエリの使用方法について、まだお困りですか?
Testing Playgroundという、とても便利なChrome拡張機能があります。この拡張機能は、要素を選択する最適なクエリを書く補助をしてくれます。
ブラウザーの開発者向け機能の中でHTML要素の階層を表示して調べることができ、それらの要素をどのように選択するべきか示してくれるため、良いテストコードの書き方を身につける助けになるでしょう。
もしこれらのクエリにより慣れ親しみたいなら、testing-playground.comで試すことができます。Testing Playgroundは様々なクエリをHTMLに試せるインタラクティブな仮想環境で、これまでに説明した要素選択のルールに対する視覚的なフィードバックを得ることができます。