Open20

Testing Library のクエリの優先度をちゃんと理解したい

かしかし

testing library の基本方針

使うライブラリがどういう思想をもって作られているかを知ることは大切。
基本方針がここに示されてる。

https://testing-library.com/docs/guiding-principles/

The more your tests resemble the way your software is used, the more confidence they can give you.
(日本語訳)
ソフトウェアのテストが使用方法に似ているほど、テストの信頼性が高くなる

testing library 自体もなるべく使用方法に近いようにテストできるような機能を提供している。 testing library が実装しているユーティリティの基本方針は次の3つ。どれも「このライブラリが極めて軽量で、シンプルで、理解しやすい」を満たすことを目的としている。

  • コンポーネントのレンダリングに関するテストをするならば、コンポーネントインスタンスではなく DOM ノードを扱うべき。コンポーネントインスタンスの使用は推奨していない
  • コンポーネントの意図した通りの方法で使用するようなテストができるよう促すべき。
  • ユーティリティの実装や API はシンプルで柔軟であるべき。

このライブラリを使う我々も、ユーザが実際に使うケースに似たテストを書くように努めたほうがよい。クエリの優先順位は、その思想の下でつけられている。

かしかし

コンポーネントのレンダリングに関するテストをするならば、コンポーネントインスタンスではなく DOM ノードを扱うべき。コンポーネントインスタンスの使用は推奨していない

例えば Button というコンポーネントのテストをするときに、

let button = <Button />;

みたいな使い方を推奨してなくて、

render(<Button />);

としましょうね、という話でいいのかな

かしかし

コンポーネントの意図した通りの方法で使用するようなテストができるよう促すべき。

例えば「ユーザが『送信』ボタンをクリックすると○○になる」みたいなテストしたいとする。「『送信』というボタンを探して、それをクリックする」という挙動を再現したテストを作れるような環境をちゃんと用意しておくよ、よいう話でいいと思う。

ライブラリを使用する我々は、このように用意してくれている環境をちゃんと利用したテストを書きましょうね、ということ

かしかし

大きな括りにおいての優先度は次の感じ

  1. 視覚、マウス操作、支援技術を使ったユーザ体験を反映したクエリ(Queries Accessible to Everyone)
  2. HTML5 と ARIA に準拠したクエリ(Semantic Queries)
  3. TestIDs を利用するクエリ(Test IDs)

優先度が付いている理由は、前述の「ユーザが実際に使うケースに似たテストを書くように努めたほうがよい」である。優先度が低いものはユーザの体験を反映しづらい。

3の優先度が低い理由は、ユーザが認識できない情報を使っているから。
2の優先度が低い理由は、これらの属性を使った場合のユーザ体験はブラウザや支援技術によって大きく異なるから。

かしかし

Queries Accessible to Everyone について

このカテゴリでの優先度はこんな感じ

  1. ロールをもとに element を探す getByRole
  2. ラベルをもとに element を探す getByLabelText
  3. プレースホルダーをもとに element を探す getByPlaceholderText
  4. テキストをもとに element を探す getByText
  5. フォームの入力された値をもとに element を探す getByDisplayValue
かしかし

ロールをもとに element を探す getByRole

他のどのクエリよりも優先度を高く考えていいらしい。
アクセシビリティツリーで確認できる element を見つけるのに使える。 name オプションを使って accessible name でフィルタをできるのもいい。
getByRole を使えない場合は、 UI のアクセシビリティが低いことを疑ったほうがいいかもしれない。

ロールとして指定できるのは ここ に示してあるものたち。

name のほかのオプションは ここ にある(あとで追う)

かしかし

ラベルをもとに element を探す getByLabelText

getByRole の次に優先度が高い。
getByLabel("name") は「"name" というラベルにくっついてる入力欄を探す」みたいなことができる。

これと同じ表現は getByRole でも getByRole('textbox', { name: "name" })とできる。なおかつ role の種類まで指定できるので、 getByRole のほうが優先度が高いとしていい。

オプションは ここ にある

getByRole ではなくて getByLabelText を使う必要ってどういうケースだろう。 getAllByLabelText(/value/) とかするとロールに関わらずすべての "** value" というラベルの element を見つけられる、みたいな使い方かな

かしかし

プレースホルダーをもとに element を探す getByPlaceholderText

プレースホルダーしか element を特定する方法がないときは他のやり方よりもマシなので使ってもいいよ、くらいの感じ

プレースホルダーは有害である という記事へのリンクがある。
ラベルをなくしてプレースホルダーだけを使って入力欄を作る危険性について述べている

挙げられている問題
  1. 入力中に何を書くべきか忘れたとき、入力した文字を消してプレースホルダーを表示させないといけない
  2. 送信前に入力内容が正しいか確認できない
  3. エラーメッセージが何に対して書かれているのかわからなくなる
  4. フォームに注目するだけでプレースホルダーが消える場合、 Tab キー移動した際に何を書いていいかわからない(注目するまでフォームが画面外にいることがある)
  5. 空のフォームよりも目に止まりにくい
  6. プレースホルダーを初期値と勘違いしてしまう可能性がある
  7. ユーザが入力された値と勘違いし、自身が入力するのに不要な手間が必要だと誤解させてしまう

「プレースホルダーしかないから getByPlaceholderText を使う」という状況が良くないので使わないほうがいい。
getByRole でも getByPlaceholderText でもどっちでもいいなら、あえて後者を使う理由は見当たらない。

getByPlaceholderText("〇山△子")よりも getByRole('textbox', {name: "名前"}) のほうがやりたいことはよくわかる

かしかし

テキストをもとに element を探す getByText

インタラクションがない element に対して使う。

たまに、 getByText("送信") みたいなことをやってしまってたんだけど、そういう場合には getByRole("button", {name: "送信"})としたほうがいい

かしかし

フォームの入力された値をもとに element を探す getByDisplayValue

The current value of a form element can be useful when navigating a page with filled-in values.
とあったけど意味わからなかった。

とりあえずあんまり出番はないと思っていいかな(使ったことない)

かしかし

Semantic Queries について

優先度は

  1. alt のアトリビュートを使うgetByAltText
  2. title のアトリビュートを使う getByTitle
かしかし

alt のアトリビュートを使うgetByAltText

alt を使って element を探す。
imginputarea にしか使えない。

ByRole の説明の中に、

For example <img aria-label="fancy image" src="fancy.jpg" /> will be returned for both getByAltText('fancy image') and getByRole('img', { name: 'fancy image' })

aria-label 属性を使うと getByRole でも getByAltText でもどちらでも探せるようになる」と解釈したんだけど本当?この場合 getByAltText は動かないっぽいんだけど...

render(<img aria-label="fancy image" src="fancy.jpg" />);
screen.getByRole("img", { name: "fancy image" }); // ok
screen.getByAltText("fancy image") // error: Unable to find an element with the alt text: fancy image

alt アトリビュートを指定するとどちらでも動く

render(<img alt="fancy image" src="fancy.jpg" />);
screen.getByRole("img", { name: "fancy image" }); // ok
screen.getByAltText("fancy image"); // ok

https://codesandbox.io/s/test-query-priority-mi4inx?file=/src/ByAltTest.test.tsx
テストが落ちる

かしかし

title のアトリビュートを使う getByTitle

title アトリビュートを参照しないスクリーンリーダーがあるかららしい。また、画面を見るユーザにも見えない情報である。この2点が title じゃなくて aria-label なんかを使って情報を渡したほうがいい理由。だから getByLabelText とかを使おうという話。

SVG もタイトルを指定できるらしい。知らなかった

https://testing-library.com/docs/queries/bytitle

かしかし

Test IDs について

テストでしか使わない ID を使うので、ユーザの動きを再現しているとは言いづらい
テキストが動的な場合に使えると書いているけどどういう意味だろう、よく分からなかった

かしかし

一通りクエリをさらった感想

testing library の優先度は「ソフトウェアのテストが使用方法に似ているほど、テストの信頼性が高くなる」という基本方針に則ってつけられている。
クエリを選ぶ場合は「どっちえも動作が確認できているから大丈夫」ではなく「こっちのほうがユーザの使用方法に近くなるから」という理由で適切なクエリを選ぶのが、いいテストを書くことにつながる。

あと優先度が低いテストを書いているならそもそもコンポーネントのアクセシビリティが悪い、ともいえる。
テストがないコンポーネントに対しては、今の段階で可能な最も高い優先度のクエリのテストを作り、そのあとに実装を修正し、再度テストを修正するのがよさそう