React Testing Libraryとは?使い方・クエリ・ベストプラクティスを実務目線で解説
React Testing Library(RTL)は、Reactコンポーネントを「実際のユーザーが画面をどう触るか」に近い形で検証するテスト用ライブラリです。stateやpropsといった内部実装ではなく、レンダリング結果のDOMに対してボタンを探し、クリックし、表示を確かめる。だからリファクタリングでテストが壊れにくい。この記事では最新の v16.3.2(2026年1月リリース)を前提に、インストールでつまずきやすい点、クエリの優先順位、user-event の書き方、そして「やってはいけない書き方」までをまとめます。React 19 と組み合わせるときの破壊的変更にも触れます。
まとめ
- RTLは「使われ方に近いテストほど信頼できる」という設計思想のライブラリ。DOM経由で検証し、内部実装には触れない。
- v16以降は
@testing-library/domを自分でインストールする。peer dependency(^10.0.0)に変わったため、入れ忘れるとモジュール解決エラーになる。公式のインストールコマンドも2パッケージ指定に変わっている。 - RTLはテストランナーではない。実行役は Jest(30.4.2)か Vitest(4.1.10)。Viteを使っているプロジェクトなら Vitest が素直。
- クエリは
getByRoleを第一候補に。getByTestIdは最後の手段。存在しないことを確かめるならqueryBy、非同期に出てくる要素はfindBy。 - user-event v14 は
awaitが必須。userEvent.setup()を render 前に呼ぶのが推奨形。fireEventは低レベルAPIで、実際のユーザー操作を再現しない。 - React 19 では
actがreactパッケージへ移動(旧経路のimportは非推奨警告)。test-utilsのその他の関数とReactDOM.renderは使えない。古い記事のコードはそのままでは動かない。 - スナップショットの多用と
container.querySelectorは避ける。壊れやすいだけで、ユーザー体験を何も保証しない。
React Testing Libraryとは:DOM経由でユーザー視点を検証するライブラリ
設計思想:使われ方に近いテストほど信頼できる
公式ドキュメントが掲げる指針は一文だけです。「The more your tests resemble the way your software is used, the more confidence they can give you(テストがソフトウェアの実際の使われ方に似ているほど、それが与えてくれる信頼は大きくなる)」。この一文がAPI設計のすべてを説明しています。RTLには「このコンポーネントのstateを取り出す」関数がありません。用意していないのは、意図的な設計判断です。
結果として、内部を書き換えてもユーザーに見える結果が同じならテストは通り続けます。クラスコンポーネントを関数コンポーネントに書き換えても、状態管理をuseStateからuseReducerに変えても、画面に「送信しました」と出るならテストは緑のまま。テストがリファクタリングの足かせにならない、というのがRTL最大の実利です。
Jest・Vitestとの役割分担:RTLは実行役ではない
混同されがちですが、RTL自体はテストを実行しません。RTLが提供するのは「コンポーネントを描画する(render)」「要素を探す(screen.getBy…)」「操作する(user-event)」の3つだけ。テストファイルを見つけて実行し、expect で結果を判定するのはテストランナーの仕事です。
| 役割 | 担当 | 最新版(2026年7月時点) |
|---|---|---|
| テスト実行・アサーション | Jest または Vitest | Jest 30.4.2 / Vitest 4.1.10 |
| コンポーネント描画・要素取得 | @testing-library/react | 16.3.2 |
| クエリ実装の本体 | @testing-library/dom | 10.4.1 |
| DOM向けの追加マッチャ | @testing-library/jest-dom | 6.9.1 |
| ユーザー操作の再現 | @testing-library/user-event | 14.6.1 |
Viteベースのプロジェクトなら Vitest を選ぶほうが設定が短く済みます。Jest向けにトランスパイル設定を書き直す必要がないためです。具体的な手順はVitestの使い方|React Testing Libraryでコンポーネントテストを書く実践ガイド【2026年版】にまとめています。
インストール:peer dependency の入れ忘れが最頻出のつまずき
domパッケージの明示インストール:v16の最重要変更
RTL v16 で、クエリの実体である @testing-library/dom が依存関係から peer dependency(^10.0.0)へ変更されました。つまり自動では入りません。公式のインストールコマンドも2パッケージを並べる形になっています。
npm install --save-dev @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event
@testing-library/dom を省くと、テスト実行時にモジュールを解決できずに落ちます。「昔のプロジェクトからコピーしたpackage.jsonで動かない」「チュートリアルどおりに入れたのに動かない」という詰まり方の典型がこれです。v15以前の記事は1パッケージ指定のままなので、そのまま真似すると踏みます。
Jestで動かす設定:jsdom環境は別パッケージ
ここでもう1つ落とし穴があります。Jestはバージョン28以降、jsdom環境を同梱していません。testEnvironment: 'jsdom' と書くだけでは Test environment jest-environment-jsdom cannot be found で落ちます。Jest本体と同じメジャー(30系)の jest-environment-jsdom を入れてください。
npm install --save-dev jest jest-environment-jsdom
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
// jest.setup.js
import '@testing-library/jest-dom';
なおJSXを含むテストを実行するには、別途トランスパイルの設定(babel-jest と @babel/preset-react、TypeScriptなら ts-jest、Next.jsなら next/jest)が必要です。上の設定は、それらがすでに用意されているプロジェクトを前提にしています。
Vitestで動かす設定:jsdomは任意依存
Vitestも同様にjsdomを同梱しません(optional peer dependency扱い)。jsdom を明示的に入れます。
npm install --save-dev vitest jsdom @vitejs/plugin-react
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './vitest.setup.ts',
},
});
// vitest.setup.ts
import '@testing-library/jest-dom/vitest';
jest-dom の読み込み先が @testing-library/jest-dom/vitest である点に注意してください。Jest用のエントリを読むと expect の拡張が効きません。
React 19の変更点:actの移動とtest-utilsの停止
React 19(最新は19.2.7)では、テストに直接関係する変更が入りました。公式のアップグレードガイドに明記されています。
actの引っ越し:react-dom/test-utilsからreactパッケージへ移動しました。旧経路からのimportは当面動きますが、「ReactDOMTestUtils.act is deprecated in favor of React.act」という非推奨警告が出ます。import { act } from 'react'へ書き換えてください。test-utilsのその他の関数は使えない:act以外のreact-dom/test-utilsの関数(renderIntoDocumentなど)は、React 19では呼び出すとエラーになり、exportも将来のバージョンで削除されると公式が明言しています。コンポーネントの内部実装に依存しやすいAPIだった、というのが理由です。ReactDOM.renderの削除:createRootへ移行が必要です。RTLのrenderを使っている限り内部で吸収されますが、自前でマウントするヘルパーを書いている場合は修正対象になります。
RTL 16.3.2 の peer dependency は react / react-dom ともに ^18.0.0 || ^19.0.0 で、React 18と19の両方を正式サポートします。バージョンを上げること自体は問題になりません。壊れるのは、古い記事から act のimport文をコピーしたときです。
最初のテスト:render と screen の使い方
RTLのテストは「描画する→探す→確かめる」の3行が基本形です。render でコンポーネントをDOMへ描画し、screen 経由で要素を探します。
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('名前を渡すと挨拶が表示される', () => {
render(<Greeting name="山田" />);
expect(screen.getByText('こんにちは、山田さん')).toBeInTheDocument();
});
render の戻り値からもクエリを取り出せますが、公式が推奨するのは screen 経由です。分割代入するクエリを増やす必要がなく、どのテストでも書き方が揃うためです。toBeInTheDocument は jest-dom が追加するマッチャで、これが「見つからない」というエラーが出たらセットアップファイルの読み込み漏れを疑ってください。
各テストの後片付け(アンマウント)は自動で走ります。afterEach(cleanup) を手書きする必要はありません。
要素の取得:getByRoleを第一候補にする
クエリ選択の優先順位:roleが第一候補
公式は明確な優先順位を示しています。上から順に検討し、上で取れるなら下は使わないのが原則です。
- 優先度1・誰にでもアクセス可能なクエリ:
getByRole(最優先)、getByLabelText(フォーム向け)、getByPlaceholderText、getByText、getByDisplayValue - 優先度2・セマンティッククエリ:
getByAltText、getByTitle - 優先度3・テストID:
getByTestId(最後の手段)
getByRole が最優先なのは、支援技術がその要素をどう認識するかと一致するからです。ボタンをroleで取れないなら、それはスクリーンリーダーからもボタンに見えていないということ。テストがアクセシビリティの検査を兼ねます。
// 推奨:アクセシブルな名前で取得する
await user.click(screen.getByRole('button', { name: '送信' }));
// 非推奨:見た目の構造に依存し、リファクタリングで壊れる
container.querySelector('.btn-primary').click();
どのroleで取れるか分からないときは、screen.debug() で現在のDOMを出力するか、@testing-library/react から個別にインポートした logRoles(container) で要素に割り当てられたroleの一覧を確認できます(logRoles は screen のメソッドではなく独立した関数です)。公式が提供するTesting Playground(screen.logTestingPlaygroundURL())を使えば、ブラウザ上で最適なクエリを提案させることもできます。
getBy・queryBy・findByの使い分け:該当なしの挙動で選ぶ
3つの接頭辞は「見つからなかったときの挙動」で選びます。ここを取り違えると、テストが落ちるべき場面で落ちなくなります。
| 接頭辞 | 該当なし | 非同期の待機 | 使いどころ |
|---|---|---|---|
| getBy… | 例外を投げる | しない | あるはずの要素 |
| queryBy… | nullを返す | しない | ないことの確認 |
| findBy… | 例外を投げる(Promise) | する(既定1000ms) | 後から現れる要素 |
「表示されていないこと」を確かめたいのに getBy を使うと、要素が無い時点で例外になってテストが書けません。ここは queryBy と toBeNull() の組み合わせです。逆に、API応答後に出るメッセージを getBy で取ろうとすると、まだ描画されていないため失敗します。findBy なら見つかるまでリトライしてくれます。
ユーザー操作:user-event v14 は await を付ける
user-event v14(14.6.1)ではすべての操作APIがPromiseを返します。await を忘れると、操作が反映される前にアサーションが走り、原因のわかりにくい失敗になります。公式が推奨する形は、render の前に setup() を呼ぶことです。
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('フォームを送信すると完了メッセージが出る', async () => {
const user = userEvent.setup();
render(<ContactForm />);
await user.type(screen.getByLabelText('メールアドレス'), '[email protected]');
await user.click(screen.getByRole('button', { name: '送信' }));
expect(await screen.findByText('送信が完了しました')).toBeInTheDocument();
});
fireEvent との違いは再現の忠実さです。fireEvent.click() はclickイベントを1つ発火させるだけですが、user.click() は実際のブラウザと同じくホバー、pointerdown、mousedown、focus、pointerup、mouseup、click という一連のイベントを順に発火します。user.type() も1文字ずつ keydown → keypress → input → keyup を送るため、入力値が実際に反映されていく過程まで再現されます。入力値の検証やdisabled解除のような「操作の過程」に依存するUIは、fireEventでは通ってしまい実際には壊れている、という取りこぼしが起きます。迷ったらuser-eventを使ってください。fireEventは、user-eventで表現できない低レベルなイベント(scrollなど)に限定するのが実務的な線引きです。
非同期処理とAPI通信のテスト
データ取得を伴うコンポーネントでは、findBy と waitFor を使い分けます。要素の出現を待つなら findBy。これで足りるケースがほとんどです。waitFor は「要素以外の条件」(モック関数が呼ばれた、など)を待つときに使います。
test('取得したユーザー名を表示する', async () => {
render(<UserProfile userId="1" />);
// ローディング中の表示を確認
expect(screen.getByText('読み込み中')).toBeInTheDocument();
// 取得完了後に現れる要素を待つ
expect(await screen.findByRole('heading', { name: '山田太郎' })).toBeInTheDocument();
});
API通信そのものは、モックに置き換えるのが基本です。Jestの jest.fn() や jest.mock() を使った関数単位のモックはJestを使用したモック関数の作成方法と基本的な使い方で詳しく扱っています。Vitestなら vi.fn と vi.mock が同じ役割です。
fetchを差し替えるよりネットワーク層でモックする
global.fetch を jest.fn() で上書きする書き方は手軽ですが、URLやリクエストボディが正しいかを検証しづらく、コンポーネント側がaxiosに変わっただけでテストが総崩れになります。実務で勧められるのは MSW(Mock Service Worker、最新2.15.0)でネットワーク層を差し替えるやり方です。アプリ側のコードは本物のfetchを呼んだままでよく、レスポンスだけを差し替えられます。
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get('/api/users/1', () => HttpResponse.json({ name: '山田太郎' })),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
エラー時の表示を試したければ、そのテストの中だけ server.use() でハンドラを差し替えて500を返させます。実装をモックするのではなく通信をモックする。この差がテストの寿命を決めます。
waitForに複数のアサーションを積まない
waitFor の中にアサーションを複数書くのは避けてください。1つ目で失敗すると、タイムアウトまでリトライを繰り返してから落ちるため、テストが遅くなるうえエラーメッセージも読みにくくなります。これは eslint-plugin-testing-library の no-wait-for-multiple-assertions ルールとしても明文化されています。待つのは1つの条件だけにし、残りの検証は waitFor の外で行います。
ベストプラクティス:やってはいけない書き方から覚える
RTLは「書ける書き方」が広いぶん、間違った使い方をしても動いてしまいます。以下は実際に壊れやすさや誤検知につながるパターンで、避けるべきものです。
container.querySelector でDOMを直接掘らない
クラス名やタグ構造でノードを取りに行くと、CSSの変更やマークアップの入れ替えでテストが落ちます。しかもそれはユーザー体験の劣化を意味しません。テストが落ちる理由は「ユーザーができなくなったこと」だけであるべきです。roleやラベルで取れないなら、まずコンポーネント側のアクセシビリティを直す。それが正しい順序です。
スナップショットテストを主軸にしない
スナップショットは差分を機械的に検出するだけで、「その差分が正しいか」は判定しません。実務では、レイアウト調整のたびに大量のスナップショットが赤くなり、中身を読まずに更新コマンドを打つ運用に落ちがちです。そうなった時点でテストは何も守っていません。使うなら、変更頻度が低く出力が小さいユニット(整形済みの文字列など)に限定してください。UIの正しさは getByRole と明示的なアサーションで確かめるほうが確実です。
act() で手動ラップしない
act() の警告が出たとき、その場しのぎで act(() => { ... }) を巻くのは対症療法です。RTLの render と user-event はすでに内部で act を通しており、警告が出るのはたいてい「awaitし忘れた非同期更新がある」というサインです。findBy や await user.click() で待つべき箇所を待っていないだけのことが多い。警告を消すのではなく、待ち漏れを直してください。
実装の内部状態をテストしない
useStateの値やpropsの受け渡しを検証したくなったら、それはユニットテストの粒度を間違えているサインです。ロジックが複雑なら、そのロジックを純粋な関数へ切り出して単体でテストする。コンポーネントのテストは「画面に何が出るか」に集中させます。
CIでの実行:watchを切りカバレッジを取る
ローカルの vitest や jest --watch はファイル変更を監視して常駐します。CI上でそのまま走らせるとジョブが終わらないため、1回実行して終了するモードを明示します。Vitestは vitest run、Jestは jest --ci です。
// package.json
"scripts": {
"test": "vitest",
"test:ci": "vitest run --coverage"
}
Jestの --ci は、スナップショットが存在しないときに自動生成せず失敗させます。「CIでは通ったのにレビューで見たら中身が空だった」という事故を防ぐための挙動です。カバレッジは目標値そのものより、数字が下がったときに理由を説明できる状態にしておくほうが実務では効きます。
コンポーネントテストで代替できない領域を見極める
RTLはjsdom上で動くため、実ブラウザ固有の挙動(レイアウト計算、実際のスクロール、複数タブ、CSSの適用結果)は検証できません。getBoundingClientRect は常に0を返します。ビジュアルの崩れや実ブラウザ差分を担保したいなら、そこはE2Eやブラウザモードの領域です。Playwright Component Testとは何か?概要と基本的な特徴を徹底解説や、実ブラウザで走らせるVitest 4の新機能と移行ガイド|ブラウザモード正式化・VRT・最新バージョンが選択肢になります。RTLで無理に見た目を守ろうとしないことが、結果的にテストの信頼性を保ちます。
よくある質問(FAQ)
React Testing Libraryとは何ですか?
Reactコンポーネントを、レンダリング後のDOMを通してユーザーの操作に近い形で検証するテスト用ライブラリです。内部のstateやpropsには触れず、画面に見えるもの・操作できるものだけを対象にします。テスト実行そのものはJestやVitestが担当します。
JestとReact Testing Libraryの違いは何ですか?
役割が違うため、比較ではなく併用します。Jestはテストランナー兼アサーションライブラリで、テストを見つけて実行し合否を判定します。RTLはコンポーネントを描画して要素を取得・操作するためのライブラリです。JestだけではReactコンポーネントを描画できず、RTLだけではテストを実行できません。
toBeInTheDocument が使えないのはなぜですか?
toBeInTheDocument は @testing-library/jest-dom が追加するマッチャで、RTL本体には含まれません。パッケージを入れたうえで、セットアップファイルで読み込む必要があります。Vitestの場合は @testing-library/jest-dom/vitest をimportし、設定の setupFiles に登録できているか確認してください。
fireEventとuserEventはどちらを使うべきですか?
原則としてuser-eventです。fireEvent が単一のイベントを発火するだけなのに対し、user-eventはホバーからフォーカス、キー入力まで実際のブラウザと同じ順序でイベントを送るため、操作の過程に依存するUIのバグを取りこぼしません。v14では userEvent.setup() を呼び、各操作を await します。fireEventはscrollなどuser-eventで表現できない場合に限定します。
@testing-library/dom が見つからないと出るのはなぜですか?
v16から @testing-library/dom がpeer dependency(^10.0.0)になり、自動では入らなくなったためです。@testing-library/react と並べて明示的にインストールしてください。公式のインストールコマンドも2パッケージを指定する形に更新されています。