「なぜそのテストは脆いのか?」React Testing Libraryのクエリ優先順位で読み解く、真に保守的なUIテストの極意
やあ。現場でコードを書いていると、テストコードが「実装の詳細」に依存しすぎて、リファクタリングのたびに真っ赤になる……なんて経験、一度や二度じゃないはずだ。
React Testing Library (RTL) が提唱する「ユーザーの視点に立つ」という哲学は、単なるスローガンじゃない。あれは、テストを「実装の監視役」から「仕様の守護神」へと昇華させるための唯一の指針なんだ。今日は、RTLのクエリメソッドをどう使い分けるべきか、その優先順位の裏側にある「ブラウザとユーザーの心理」を深掘りしていこう。
—
1. なぜ「ユーザーの視点」が最強なのか?
まず、大前提を共有したい。DOM構造(`div`が何個あるか、`className`が何か)なんて、ユーザーには全く関係ない。ユーザーが気にするのは「どこにボタンがあるか」「何を入力すればいいか」だ。
RTLのクエリ優先順位は、この「ユーザーがページをどう体験するか」をそのままコードに落とし込んでいる。実装をいじっても、ユーザー体験(UI)が変わらなければテストは落ちない。これが、壊れにくいテストの正体だ。
優先順位のヒエラルキー
公式ドキュメントでも推奨されているこの順序、丸暗記するんじゃなくて「どれだけユーザーにとって自然なアクセスか」で判断してくれ。
1. `getByRole`: ユーザーが要素を認識する方法(WAI-ARIAの役割)。
2. `getByLabelText`: フォームのラベル。
3. `getByPlaceholderText`: プレースホルダー(補助的)。
4. `getByText`: 表示されているテキスト。
5. `getByTestId`: どうしても他に方法がない時の「緊急脱出用ハッチ」。
—
2. クエリメソッドの使い分け:その「動機」を理解する
`getBy`, `queryBy`, `findBy` は、それぞれ「DOMに何が起きているか」をどう監視するかが違う。
- `getBy…`: 要素が「あるはずだ」という強い確信がある時に使う。なければ即座にエラーを吐く。これがデフォルトだ。
- `queryBy…`: 要素が「存在しないこと」を確認したい時に使う(例:モーダルを閉じた後のDOMチェック)。
- `findBy…`: 非同期処理(APIレスポンス待ちなど)の後の出現を待つ時に使う。`waitFor` のラッパーだと思えばいい。
実践的なコード例
例えば、ユーザーがボタンを押して非同期で結果を表示するコンポーネントをテストしてみよう。
import { render, screen, fireEvent } from ‘@testing-library/react’;
import UserProfile from ‘./UserProfile’;
test(‘ユーザー名が非同期で表示されることを確認する’, async () => {
render(
// 1. 初期状態:ローディング中であることを確認(getByRole)
// ユーザーは「読み込み中」というステータスを認識できるべき
const loading = screen.getByRole(‘status’);
expect(loading).toBeInTheDocument();
// 2. 非同期処理後:ユーザー名が表示されるのを待つ(findByRole)
// findBy系は内部でwaitForを使っているので、待機が必要な時に最適
const userName = await screen.findByRole(‘heading’, { name: /takuya/i });
expect(userName).toBeInTheDocument();
// 3. 存在しないことを確認(queryByRole)
// 読み込みインジケーターが消えたことを確認する際、getByを使うとエラーになるためqueryByを使う
const goneLoading = screen.queryByRole(‘status’);
expect(goneLoading).not.toBeInTheDocument();
});
—
3. 現場で「やってはいけない」こと
よくある失敗例が、`container.querySelector(‘.btn-primary’)` のようなセレクタによる取得だ。これはCSSクラスやDOM階層に強く依存している。デザイナーがクラス名を変更しただけでテストが落ちるなんて、保守性ゼロだ。
また、`data-testid` を多用しすぎるのも禁物。「テストのために本番コードを汚染する」ことになる。`data-testid` は、あくまで「どうしてもRoleやTextで取れない複雑なカスタムコンポーネント」のために取っておくのが、シニアな戦い方だ。
—
4. チーフからのアドバイス:アクセシビリティはテストを楽にする
ここが一番面白いところなんだが、「テストしやすいコード」は「アクセシブルなコード」と完全に一致する。
`getByRole` で要素を取るためには、HTMLが適切にマークアップされている必要がある。
- `button` タグを使っているか?
- `input` に `id` と `label` が紐付いているか?
- 適切な見出しレベル(h1~h6)を使っているか?
これらを意識するだけで、テストコードは劇的にシンプルになる。アクセシビリティを「義務」と捉えず、「テストを楽にするためのツール」として活用してみてほしい。そうすれば、チームの成果物の品質は、自然と世界水準に引き上げられるはずだ。
—
まとめ:次にテストを書くときへのチェックリスト
1. 「ユーザーがどうやってこの要素を見つけるか?」をまず考える。
2. `getByRole` で取れないか検討する(`aria-label` も活用せよ)。
3. 非同期なら `findBy` を躊躇せず使う。
4. `queryBy` は「存在しないことの証明」に限定する。
5. `data-testid` は最後の手段。
テストコードは君の書いた機能を守る城壁だ。その設計が甘ければ、少しの変更で城は崩れる。だが、堅牢に築けば、君は自信を持ってデプロイボタンを押せるようになる。
さあ、エディタを開いて、より「人間味のある」テストコードを書きに行こう。何か詰まったら、いつでも聞いてくれ。

コメント