React Testing Library(RTL)は、Reactコンポーネントを描画したDOMに対して、ユーザーと同じように要素を探し、操作し、表示を確かめるためのテスト用ライブラリです。stateやpropsには触れません。そのため内部実装を書き換えてもテストが壊れにくくなります。この記事では2026年10月28日時点の最新版 @testing-library/react 16.3.3 を前提に、導入から最初のテスト、Vitest 5・Jest 30での設定、つまずきやすいエラーの原因、避けるべき書き方までを扱います。エラーメッセージはローカルの検証用コードで再現しています。後述の表は、その条件で確認した原因と対処例です。
まとめ
- RTLはテストランナーではありません。描画・要素の取得・操作を担当し、実行と判定は Vitest(5.0.5)か Jest(30.5.2)が担います。
- インストールは
@testing-library/domを並べて指定します。v16からpeer dependencyになり、npm 11は自動で入れますが、--legacy-peer-depsでは入らずCannot find moduleで落ちます。 - Vitestは
globals: trueにしないと自動cleanupが走りません。2本目のテストでFound multiple elementsが出たら、cleanup未実行のほか、同じ条件に一致する要素が画面内に複数ないかも確認します。 - jest-dom 7.0 は Node.js 22以上が必須。Node 20で動かしているJestプロジェクトは6.9.1に留めます。Vitestでは
@testing-library/jest-dom/vitestを読み込みます。 - クエリは
getByRoleが第一候補、user-event はawait必須。使い分けの詳細はuser-eventとクエリの実践記事に分けています。 - React 19では
actをreactからimportします。react-dom/test-utilsの他の関数は削除済みです。 - スナップショットの多用と
container.querySelectorは避けます。これらだけに頼ると、ユーザー操作の成否を十分に検証できず、実装上の変更でテストが壊れやすくなります。
React Testing Libraryとは:DOM経由でユーザー視点を検証するライブラリ
RTLの設計思想:ユーザーの使い方に沿う検証
Testing Library全体の指針は、公式のGuiding Principlesにある一文に集約されています。「The more your tests resemble the way your software is used, the more confidence they can give you.(テストがソフトウェアの実際の使われ方に近いほど、そのテストから得られる確信は大きくなる)」。RTLに「コンポーネントのstateを取り出す関数」が無いのは、この方針による意図的な設計です。
画面に「送信しました」と出る限り、クラスコンポーネントを関数コンポーネントへ書き換えても、useStateをuseReducerへ変えても、テストは通り続けます。リファクタリングのたびにテストを直す必要がない。これがRTLを選ぶ最大の理由です。Kent C. Dodds が提唱したテスト配分の考え方はTesting Trophy(テスティングトロフィー)とは?Kent C. Doddsの4層とテストピラミッドとの違いで扱っています。
RTLとJest・Vitestの役割分担:DOM操作とテスト実行
RTLが提供するのは「描画する(render)」「探す(screen.getBy…)」「低レベルのイベントを発火する(fireEvent)」などです。ユーザー操作を再現するuser-eventは別パッケージとして追加します。テストファイルを見つけて実行し、expect で合否を判定するのはテストランナーの仕事になります。2026年10月28日にnpm registryで確認した各パッケージの最新版は次のとおりです。
| 役割 | パッケージ | 最新版 | 公開日 |
|---|---|---|---|
| 描画・要素取得 | @testing-library/react | 16.3.3 | 2026-08-27 |
| クエリの実装本体 | @testing-library/dom | 10.4.2 | 2026-09-13 |
| DOM向けマッチャ | @testing-library/jest-dom | 7.0.1 | 2026-08-09 |
| ユーザー操作の再現 | @testing-library/user-event | 14.6.8 | 2026-10-13 |
| テストランナー | vitest / jest | 5.0.5 / 30.5.2 | 10-15 / 09-18 |
| DOM環境 | jsdom | 30.1.3 | 2026-10-15 |
16.3.3 は GitHubのリリースノートによると、イベント発火時の act() の再入を避けるバグ修正のみです。16.3系の中で書き方を変える必要はありません。peer dependencyは react ・ react-dom ともに ^18.0.0 || ^19.0.0 で、React 18と19の両方に対応しています。
RTLのインストール:@testing-library/dom の明示指定
公式のインストール手順は、2パッケージを並べる形です。マッチャと操作用のパッケージも同時に入れておくと、次の設定にそのまま進めます。
npm install --save-dev @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event
パッケージ管理ツールで変わる dom の入り方
RTL v16で @testing-library/dom は通常の依存からpeer dependency(^10.0.0)へ移りました。peer dependencyを自動で入れるかどうかはツールと設定で変わるため、「同じpackage.jsonなのに自分の環境だけ落ちる」原因になります。@testing-library/react と react だけを指定してインストールした結果は次のとおりでした。
| インストール方法 | dom 10.4.2 | RTLのimport |
|---|---|---|
| npm 11.17.0(既定) | 自動で入る | 成功 |
| pnpm 11.15.1(既定) | .pnpm内にだけ入る | 成功 |
| npm –legacy-peer-deps | 入らない | MODULE_NOT_FOUND |
npmとpnpmの既定では動いてしまうため、package.jsonに @testing-library/dom が書かれていないことに気付きにくい点が厄介です。古いプロジェクトで依存衝突を避けるために --legacy-peer-deps を付けている環境や、CIで別の設定が効いている環境では、そこで初めて Cannot find module '@testing-library/dom' が出ます。devDependenciesに明記すると、peer dependencyの自動インストール設定に依存せず、必要なパッケージを導入できます。
Vitestで動かす設定:globals と jest-dom の読み込み先
Viteを使うプロジェクトならVitestが素直な選択です。jsdomはVitestに同梱されていないので明示的に入れます。jsdom 30.1.3 のengines指定は ^22.22.2 || ^24.15.0 || >=26.0.0 で、Vitest 5 の下限(22.12.0)より厳しい点に注意してください。
npm install --save-dev vitest jsdom @vitejs/plugin-react
// vitest.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './vitest.setup.js',
},
});
// vitest.setup.js
import '@testing-library/jest-dom/vitest';
globals: true は書き味の好みではなく、cleanupに関わる設定です。公式のセットアップ解説のとおり、RTLは「テストフレームワークがグローバルな afterEach を提供する場合」にだけ、各テスト後の自動cleanupを登録します。Vitestの既定は globals: false なので、この設定を省くと前のテストで描画したコンポーネントが残ります。globals を使いたくない場合は、セットアップファイルで afterEach(cleanup) を自分で登録します。
jest-domの読み込み先は @testing-library/jest-dom/vitest です。globals を有効にしていない構成でJest用の @testing-library/jest-dom を読むと、ReferenceError: expect is not defined でテストが1本も実行されずに止まりました(globals: true なら素のエントリでも通りますが、公式READMEがVitest向けに案内しているのは /vitest 付きです)。Vitestでの書き方全般はVitestの使い方と書き方:読み方・設定からReact Testing Libraryのコンポーネントテストまで【Vitest 5対応】にまとめています。
Jestで動かす設定:jsdom環境は別パッケージ
Jestは28以降、jsdom環境を同梱していません。Jest 30.5.2 で testEnvironment: 'jsdom' だけを書くと、次のエラーで止まります。
● Validation Error:
Test environment jest-environment-jsdom cannot be found.
As of Jest 28 "jest-environment-jsdom" is no longer shipped by default, make sure to install it separately.
Jest本体と同じ版の 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)が要ります。上の設定は、それらが用意済みのプロジェクトを前提にしています。Jestはグローバルに afterEach を持つので、cleanupは自動で走ります。
jest-dom 7.0 の要件:Node.js 22以上と dom 10系
2026年7月20日公開の jest-dom 7.0.0 の破壊的変更は2点です。リリースノートの記述は「@testing-library/dom is now a required peer dependency. The minimum supported Node.js version is now 22.」で、7.0.1のpeer dependencyは @testing-library/dom が >=10 <11 です。
Jest 30.5.2 自体はNode.js 18.14以上で動くため、Node 20でJestを使っているプロジェクトはjest-domだけが要件を満たさなくなります。Nodeを上げられないなら、engines.node が >=14 の 6.9.1 に固定してください。同日に出た 6.10.0 もNode 22以上の指定なので、6系の最新へ上げても回避になりません。7.0で増えたマッチャと移行の確認点はReact Testing Libraryのuser-eventとクエリ実践|getByRole・findBy・fireEventの使い分けで扱っています。
最初のテスト:render・screen・アサーションの3行
RTLのテストは「描画する、探す、確かめる」の3行が基本形です。render でコンポーネントをDOMに描画し、screen 経由で要素を探します。
import { render, screen } from '@testing-library/react';
const Greeting = ({ name }) => <p>こんにちは、{name}さん</p>;
test('名前を渡すと挨拶が表示される', () => {
render(<Greeting name="山田" />);
expect(screen.getByText('こんにちは、山田さん')).toBeInTheDocument();
});
render の戻り値からもクエリを取り出せますが、公式が勧めるのは screen 経由です。分割代入を増やさずに済み、どのテストでも書き方が揃います。toBeInTheDocument は jest-dom が追加するマッチャで、RTL本体には含まれません。
クエリとユーザー操作の基本:getByRoleと await user
公式のクエリ優先順位は、誰にでもアクセスできるクエリ(getByRole・getByLabelText・getByPlaceholderText・getByText・getByDisplayValue)、意味に基づくクエリ(getByAltText・getByTitle)、最後に getByTestId の順です。roleで取得できない場合は、アクセシビリティ上の問題に加え、検索範囲・name条件・描画タイミングも確認します。roleを使うテストはアクセシビリティの一部を点検できますが、スクリーンリーダーでの実際の利用を保証するものではありません。
接頭辞は該当なしのときの挙動で選びます。あるはずの要素は getBy、無いことの確認は queryBy、後から現れる要素は findBy(既定で1000ミリ秒待つ)です。user-event v14 はすべての操作がPromiseを返すので、await を付けます。
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();
});
await を外して user.click() の直後に表示を確かめると、クリックが反映される前にアサーションが走り、Unable to find an element with the text で失敗しました。input type別に取れるroleの対応、within() による範囲の限定、fireEvent を選ぶ場面、type と keyboard の書き分けはReact Testing Libraryのuser-eventとクエリ実践|getByRole・findBy・fireEventの使い分けで詳しく解説しています。
非同期処理とAPI通信のテスト
findByとwaitForの使い分け
要素の出現を待つなら findBy で足ります。waitFor は「モック関数が呼ばれた」のような要素以外の条件を待つときに使います。waitFor はコールバック内のアサーションが失敗すると再試行し、すべて成功するかタイムアウトするまで待ちます。同じ非同期対象への検証を中に複数並べると、そのうち1つが満たされない限りタイムアウトまで待つことになるため、追加の検証は waitFor の外へ出します。eslint-plugin-testing-library の no-wait-for-multiple-assertions ルールで、同じ非同期対象に対する複数のアサーションを検出できます。独立した非同期対象へのアサーションは許容されます。
test('取得したユーザー名を表示する', async () => {
render(<UserProfile userId="1" />);
// ローディング中の表示を確認
expect(screen.getByText('読み込み中')).toBeInTheDocument();
// 取得完了後に現れる見出しを待つ
expect(await screen.findByRole('heading', { name: '山田太郎' })).toBeInTheDocument();
});
MSW 3によるネットワーク層のモック
global.fetch を vi.fn() や jest.fn() で上書きする方法は手軽ですが、fetchの呼び出し引数でURLやリクエストボディを検証できますが、axiosへの変更ではfetchモックに依存するテストの見直しが必要になります。MSW(Mock Service Worker)でネットワーク層を差し替えれば、アプリ側は本物の 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({ onUnhandledFrame: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
2026年9月に出た MSW 3.0.0 では、onUnhandledRequest オプションが onUnhandledFrame に改名され、Node.jsの下限が22になり、ESMのみの配布になりました。2系の記事のまま onUnhandledRequest: 'error' と書いた場合、型チェックを行わないJavaScriptでは旧名が無視されます。TypeScriptでは、直接渡したオブジェクトの未知のプロパティとして検出される場合があります。ローカルに立てたHTTPサーバーで試すと、旧名ではハンドラの無い通信がそのまま実サーバーへ届き、応答本文が返ってきました。上のコードをMSW 3.0.3・Vitest 5.0.5で実行すると、未定義の /api/users/2 への通信は Cannot bypass a request when using the "error" strategy for the "onUnhandledFrame" option で止まりました。エラー時の表示を試すときは、そのテストの中だけ server.use() でステータス500を返すハンドラに差し替えます。モックの方式の選び方はAPIモッキングとは?モックサーバーの方式選定と契約ドリフト対策を実装目線で解説、関数単位のモックはJest mockImplementationの使い方|戻り値・非同期・実装差し替えを実務目線で解説が参考になります。
React 19で変わったテストコード:actの移動とtest-utilsの削除
React 19のアップグレードガイドには、テストに直接効く変更が3つあります。最新の React 19.3.1 でも扱いは同じです。
actはreactパッケージへ移動:react-dom/test-utilsからのimportは動きますが、開発ビルドで「ReactDOMTestUtils.actis deprecated in favor ofReact.act」という警告をconsole.errorに出します(19.3.1 のreact-dom-test-utils.development.jsで確認)。import { act } from 'react'へ書き換えます。act以外のtest-utilsは削除:ガイドは「All other test-utils functions have been removed.」と明記しています。renderIntoDocumentなどを使った古いテストはRTLのrenderへ置き換えます。ReactDOM.renderも削除:react-dom/clientのcreateRootへ移行します。RTLのrenderを使っていれば内部で吸収されますが、自前のマウント用ヘルパーは修正が必要です。
エラーメッセージ別の原因と直し方
RTLの導入で出るエラーの多くは、テストコードではなく設定の抜けが原因です。Node.js 26.5.0、Vitest 5.0.5、Jest 30.5.2の環境で再現し、表示されたメッセージの冒頭を並べました。
| メッセージの冒頭 | 原因 | 直し方 |
|---|---|---|
| Cannot find module ‘@testing-library/dom’ | peer dependencyが未導入 | domを明示してインストール |
| Test environment jest-environment-jsdom cannot be found | Jest 28以降は非同梱 | jest-environment-jsdomを追加 |
| Invalid Chai property: toBeInTheDocument | Vitestでjest-dom未読込 | jest-dom/vitestをsetupFilesへ |
| ReferenceError: expect is not defined | globals無効で素のjest-domを読込 | 読み込み先を/vitestに変更 |
| Found multiple elements with the role | 複数一致・cleanup未実行など | 検索条件・範囲・cleanupを確認 |
| Unable to find an element with the text | 未描画・検索条件不一致・待機漏れなど | DOM・検索条件・非同期処理を確認 |
| ReactDOMTestUtils.act is deprecated | 旧経路のact | actをreactからimport |
判断を誤りやすいのは5行目です。1本目のテストは通り、2本目で同じボタンが2つ見つかって落ちるため、コンポーネント側の不具合を疑ってしまいます。テストを単独で実行すると通り、ファイル全体で落ちるならcleanupを疑ってください。2行目は親切にもメッセージに直し方が書かれていますが、3行目と4行目は原因に触れていないため、jest-domの読み込み先を知らないと遠回りします。
ベストプラクティス:やってはいけない書き方
RTLは書ける幅が広く、誤った使い方でもテストは動いてしまいます。壊れやすさや誤検知につながる書き方を挙げます。
container.querySelectorへの依存とDOM構造変更の影響
クラス名やタグ構造でノードを取ると、CSSやマークアップの変更でテストが落ちます。それはユーザー体験の劣化を意味しません。テストが落ちる理由は「ユーザーができなくなったこと」だけであるべきです。roleやラベルで取れないなら、先にコンポーネント側のアクセシビリティを直します。どのroleで取れるか分からないときは、screen.debug() でDOMを出力するか、@testing-library/react から logRoles をimportして logRoles(container) で一覧を出します。
スナップショットテストの適用範囲と更新運用
スナップショットは差分を検出するだけで、その差分が正しいかは判定しません。レイアウト調整のたびに大量に赤くなり、中身を読まずに更新する運用に落ちた時点で、何も守らなくなります。使うなら出力が小さく変更頻度の低い対象に絞り、UIの正しさは getByRole と明示的なアサーションで確かめます。更新運用の決め方はスナップショットテストとは?JestとVitestの実装と更新運用を実装者向けに解説を参照してください。
act()の重複ラップ回避と手動実行が必要な更新
act() の警告が出たときに、render やuser-eventの呼び出しを act(() => { ... }) で包み直すのは対症療法です。RTLの render とuser-eventは内部で act を通しているので、警告はたいてい「待つべき非同期更新を待っていない」というサインです。findBy や await user.click() で待機を補うと解消する場合があります。ただし、RTLのヘルパー外で状態更新を起こす処理には、手動の await act(async () => { ... }) が必要な場合もあります。
コンポーネントテストの検証対象:内部状態より表示結果
useStateの値やpropsの受け渡しを検証したくなったら、テストの粒度が合っていません。複雑なロジックは純粋な関数へ切り出して単体でテストし、コンポーネントのテストは「画面に何が出るか」に集中させます。
CIでのテスト実行:watch無効化とカバレッジ計測
ローカルの vitest や jest --watch はファイル変更を監視して常駐します。CIでは1回実行して終了するモードを明示します。Vitestは vitest run(環境変数 CI が真なら vitest も1回で終了)、Jestは jest --ci です。Jestの --ci は、スナップショットが無いときに自動生成せず失敗させます。
// package.json
"scripts": {
"test": "vitest",
"test:ci": "vitest run --coverage"
}
Vitestの --coverage は @vitest/coverage-v8 を別途入れておく必要があります。テストの自動実行やカバレッジの閾値をパイプラインに組み込む作業は、DevOps・CI/CD導入支援でも支援しています。
RTLで検証できない領域と代わりの手段
本記事のjsdom構成では、レイアウト計算や実際のスクロール、画面の描画結果は検証できません。ただし、対応するCSSプロパティの計算済みスタイルなどは確認できます。getBoundingClientRect は幅も高さも0を返します。見た目や実ブラウザでの差分を守りたいなら、jsdomで無理をせずブラウザで動く手段へ切り替えます。
- Vitest Browser Mode:Playwrightなどで実ブラウザを起動してテストを実行します。React向けには
vitest-browser-react(2.3.0・Vitest 4と5に対応)があり、RTLに近い書き方で描画できます。導入はVitest Browser Modeの使い方|Playwrightでの導入・headless設定とjsdomとの使い分け【Vitest 5対応】で解説しています。 - Playwright Component Testing:コンポーネント単位で実ブラウザに描画します。1.62でstory方式へ移行しているため、Playwright Component Testingの使い方|1.62のstory方式と移行手順を確認してから採用を判断してください。
線引きの目安は単純です。本記事で扱った表示内容・操作・状態遷移は、RTLとjsdomで検証できます。レイアウトや実ブラウザ固有の挙動が絡むテストだけをブラウザ側へ移すと、テスト全体の実行時間を抑えたまま見た目も守れます。
よくある質問(FAQ)
React Testing Libraryとは何ですか?
Reactコンポーネントを描画したDOMに対して、ユーザーの操作に近い形で検証するテスト用ライブラリです。stateやpropsには触れず、画面に見えるもの・操作できるものだけを対象にします。テストの実行と判定はJestやVitestが担当します。
JestとReact Testing Libraryの違いは何ですか?
役割が違うため、どちらかを選ぶのではなく併用します。Jestはテストを見つけて実行し合否を判定するテストランナーで、RTLはコンポーネントを描画して要素を取得・操作するライブラリです。Jestだけではコンポーネントを描画できず、RTLだけではテストを実行できません。
VitestでもReact Testing Libraryは使えますか?
使えます。environment: 'jsdom' と globals: true を設定し、セットアップファイルで @testing-library/jest-dom/vitest を読み込みます。globals を有効にしない場合は、自動cleanupが走らないため afterEach(cleanup) を自分で登録してください。
toBeInTheDocument が使えないのはなぜですか?
toBeInTheDocument は @testing-library/jest-dom のマッチャで、RTL本体には含まれません。Vitestで読み込み漏れがあると Invalid Chai property: toBeInTheDocument になります。セットアップファイルでの読み込みと、Vitestなら読み込み先が /vitest 付きかを確認してください。
@testing-library/dom が見つからないと出るのはなぜですか?
v16から @testing-library/dom がpeer dependencyになったためです。npm 11やpnpmの既定では自動で入りますが、--legacy-peer-deps を付けた環境では入らずにエラーになります。@testing-library/react と並べてdevDependenciesに明記してください。
関連記事
- React Testing Libraryのuser-eventとクエリ実践|getByRole・findBy・fireEventの使い分け
- Vitestの使い方と書き方:読み方・設定からReact Testing Libraryのコンポーネントテストまで【Vitest 5対応】
- APIモッキングとは?モックサーバーの方式選定と契約ドリフト対策を実装目線で解説
- Vitest Browser Modeの使い方|Playwrightでの導入・headless設定とjsdomとの使い分け【Vitest 5対応】
- Testing Trophy(テスティングトロフィー)とは?Kent C. Doddsの4層とテストピラミッドとの違い