テスト

React Testing Libraryとは?使い方・Vitest・Jestの設定とベストプラクティス【v16.3.3】

React Testing Libraryとは?使い方・Vitest・Jestの設定とベストプラクティス【v16.3.3】

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.act is deprecated in favor of React.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で無理をせずブラウザで動く手段へ切り替えます。

線引きの目安は単純です。本記事で扱った表示内容・操作・状態遷移は、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に明記してください。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.27 テックブログ Brotli 脆弱性:CVE-2026-111497の影響確認とNode.js・Python・.NETの更新手順
  2. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処
  3. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  4. 2026.10.23 テックブログ Renta!はいつ再開?不正アクセスでメールアドレス等が流出したおそれ・再開時のパスワード再設定と運営側の対応
  5. 2026.10.28 テックブログ 楽天トラベル不正アクセス:最大約260万件の対象確認とカード下4桁の詐欺対策

RELATED POSTS 関連記事

目次