mcpField
W
web-testing-react-testing-library

web-testing-react-testing-library

github.com/agents-incテスト・品質2668 回インストール2 日前に更新
リポジトリ

web-testing-react-testing-library について

React Testing Library を使う開発者向けに、コンポーネント、フック、非同期 UI のテストパターンを解説するガイドです。実装の詳細ではなく、アクセシブルなクエリとユーザー操作の再現で見える振る舞いを検証します。

主な機能

  1. 01アクセシビリティ優先のクエリ選択
  2. 02userEvent による操作の再現
  3. 03非同期要素とアサーションの処理
  4. 04renderHook によるフックのテスト
  5. 05プロバイダーのラップとクエリ範囲の限定
  6. 06キーボード操作と ARIA 状態の検証

活用シーン

  1. 01フォーム入力やボタン操作のテスト
  2. 02読み込み・エラー・成功状態の確認
  3. 03アプリのプロバイダー付きコンポーネントのテスト
  4. 04キーボード操作とアクセシビリティの確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/agents-inc/skills --skill web-testing-react-testing-library

インストール後、Claude Code で/web-testing-react-testing-libraryを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

metadata.yaml
reference.md
examples/
accessibility.md
async-testing.md
configuration.md
core.md
custom-render.md
hooks.md
scoped-queries.md
user-events.md

詳細

React Testing Library のパターン

クイックガイド: アクセシビリティツリーを通じてクエリを実行し(まず getByRole、最後に getByTestId)、fireEvent ではなく userEvent で操作し、要素が非同期に現れる場合は findBy* を使います。v14 以降、userEvent は非同期になり、先に setup() を呼び出す方式のため、すべての呼び出しで await が必要です。このライブラリが担当するのはレンダリングと操作です。アサーション API、モック関数、フェイクタイマーは、テストファイルを実行するテストランナーから提供されます。

詳細リソース:

  • examples/core.md — クエリの優先順位、ロール、ラベル、テキスト、テスト ID の使い分け
  • examples/user-events.md — テキスト入力、クリック、キーボードショートカット、タブ順序、および fireEvent が引き続き必要なケース
  • examples/async-testing.md — findBy*、waitFor、waitForElementToBeRemoved
  • examples/custom-render.md — アプリのプロバイダーでラップしたレンダリングと、テストごとの初期状態の指定
  • examples/hooks.md — renderHook、rerender、unmount、ラッパー、例外をスローするフック
  • examples/accessibility.md — キーボードナビゲーション、ARIA 属性、ライブリージョン、フォーカストラップ
  • examples/scoped-queries.md — テーブル、カード、セクション、モーダルで使う within()
  • examples/configuration.md — フェイクタイマー、ポインターイベントのチェック、タイムアウトの上書き
  • reference.md — クエリとロールの一覧、userEvent のメソッド、キーボード構文、マッチャー一覧、デバッグ用チェックリスト

該当する方法を選ぶ

  • コンポーネントが対象の場合 — レンダリングして、生成された DOM をクエリします。 examples/core.md と examples/user-events.md を 参照してください。
  • 単独のフックが対象の場合 — クエリ対象の DOM がないため、renderHook で result.current を参照します。 examples/hooks.md を参照してください。フックが UI 内でのみ意味を持つ場合は、代わりに コンポーネントをテストします。
  • 対象にプロバイダーが必要な場合 — テーマ、認証、ルーティングなどは、テストごとに設定するのではなく、 カスタムレンダーで一度ラップします。examples/custom-render.md を参照してください。

<critical_requirements>

React Testing Library のコードを書く前に

アクセシビリティの優先順位に沿ってクエリを選びます。まず getByRole、次に getByLabelText、その次に getByText を使い、ほかの方法では要素に到達できない場合に限り getByTestId を使います。 クエリでフォールバックが必要になるなら、その要素は支援技術からも到達できないことを示しています。

最初の操作を行う前に userEvent.setup() を呼び出し、すべてのメソッドを await します。 v14 以降、これらのメソッドはプロミスを返すため、呼び出しを await しないと、DOM の更新が完了する前にアサーションが実行されます。

非同期で表示されるものには findBy* を使います。 findBy* は再試行し、処理を諦めた時点の DOM を報告します。一方、getBy* を waitFor で囲むと、最後にスローされたエラーだけが報告されます。

render が返すオブジェクトではなく、screen を使ってクエリします。 どのクエリにも同じインポートを使えます。また、分割代入を使う方法では、テストが成長するにつれて足並みがそろわなくなります。

ユーザーが見たり操作したりできるものをアサートします。 内部状態、ref、インスタンスフィールドは、動作を変えずにリファクタリングするだけで変わる可能性があります。

</critical_requirements>


自動検出: @testing-library/react, @testing-library/user-event, @testing-library/dom, render, screen, userEvent.setup, fireEvent, waitFor, waitForElementToBeRemoved, findByRole, getByRole, getByLabelText, queryByRole, renderHook, result.current, within, cleanup, prettyDOM, logRoles, logTestingPlaygroundURL, testIdAttribute, asyncUtilTimeout

適用対象:

  • React コンポーネントをレンダリングし、DOM に出力される内容をアサートする
  • シミュレートされたユーザー操作を通じてフォーム、キーボードナビゲーション、ポインター操作を行う
  • 非同期に解決される読み込み、エラー、成功状態をテストする
  • カスタムフックを単独でテストし、単独でのテストが不要な場合を判断する
  • アプリのプロバイダーを含むレンダリング環境を構築する
  • インタラクティブな要素にロール、ラベル、キーボードでアクセスできることをアサートする

別途扱うもの:

  • テストランナーの仕組み — ファイルの describe/it、アサーション API、モック関数、フェイクタイマー、セットアップファイルはテストランナーの担当です。このスキルでは、タイマーを進める関数をオプションに渡す必要がある箇所のみを取り上げます
  • ネットワークのテストダブル — コンポーネントは通信手段を認識しないため、レスポンスは通信をインターセプトする仕組みによって用意します
  • ナビゲーションや実際のバックエンドをまたぐ一連の操作 — ブラウザーを使ったエンドツーエンドテストで扱います
  • レンダリングされたフレームが引き続き正しく見えるかどうか — 承認済み画像とのピクセル比較で扱います

基本理念

このライブラリが掲げる指針は、テストがソフトウェアの使われ方に似ているほど、得られる信頼性が高まるというものです。ここから三つの帰結が導かれます。

クエリは、テストを兼ねたアクセシビリティ監査です。 getByRole("button", { name: /submit/i }) でアクセスできるコンポーネントは、同じ理由でスクリーンリーダーからもアクセスできます。機能するクエリが getByTestId だけなら、テストの問題ではなく UI の欠陥を報告していることになります。

実装の詳細とは、リファクタリング後も変わらず残るものです。 状態の形、フックの内部、クラス名、DOM の入れ子構造はすべて自由に変更できます。レンダリングされたテキスト、ロール、アクセシブルネームが契約となります。

DOM は実物です。 コンポーネントは実際のドキュメントにレンダリングされるため、コンポーネントとその子要素の間にある統合上のバグは、本番環境ではなくここで明らかになります。


基本パターン

パターン 1:クエリの優先順位

マークアップ上で便利な方法ではなく、ユーザーが要素を見つける方法に基づいてクエリを選びます。

screen.getByRole("button", { name: /sign in/i }); // interactive elements
screen.getByLabelText(/password/i); // form fields, incl. password inputs (no textbox role)
screen.getByText(/welcome back/i); // non-interactive content
screen.getByDisplayValue("draft title"); // a field by its current value
screen.getByAltText(/profile photo/i); // images
screen.getByTestId(`product-${generatedId}`); // last resort: genuinely generated content

完全なコード:examples/core.md

パターン 2:fireEvent より userEvent

fireEvent は DOM イベントを一つディスパッチします。userEvent は、実際の操作で発生する一連のイベントを、ポインター、フォーカス、キーボードの順に再現します。ハンドラーの不具合が実際に潜んでいるのは、この一連の処理です。

import userEvent from "@testing-library/user-event";

const user = userEvent.setup(); // before the first interaction
await user.type(screen.getByLabelText(/email/i), "[email protected]");
await user.click(screen.getByRole("button", { name: /create account/i }));

fireEvent は、ユーザーが発生させるものではないイベント(resize、scroll、制御したいプロパティを持つ合成イベント)に限って使います。

完全なコード:examples/user-events.md

パターン 3:非同期ユーティリティ

要素が表示されるのを待つ場合は findBy*、待つ対象が要素ではなくアサーションの場合に限り waitFor、要素が消えるのを待つ場合は waitForElementToBeRemoved を使います。

expect(await screen.findByText(/results/i)).toBeInTheDocument();

await waitFor(() => {
  expect(handler).toHaveBeenCalledWith("react"); // one assertion, no side effects
});

await waitForElementToBeRemoved(() => screen.queryByRole("progressbar"));

waitFor はコールバックが例外を投げなくなるまでポーリングします。そのため、2つ目のアサーションを中に入れると、最初の失敗が報告される前に再試行のコストがすべて発生し、副作用を中に入れるとポーリングのたびに実行されます。

完全なコード:examples/async-testing.md

パターン 4:renderHook を使ったフックのテスト

renderHook はコンポーネントなしでフックをマウントします。result.current はアクセスするたびに再読み込みされるため、値ではなくアクセスを保持してください。

const { result, rerender, unmount } = renderHook(
  ({ key }) => useLocalStorage(key, "initial"),
  { initialProps: { key: "draft" } },
);

act(() => {
  result.current[1]("updated");
});
expect(result.current[0]).toBe("updated");

フックと UI が切り離せない場合は、コンポーネントのテストを優先してください。公開するフックや、コンポーネントでは見えにくくなるエッジケースがあるフックでは、renderHook が役立ちます。

完全なコード:examples/hooks.md

パターン 5:プロバイダーを使ったカスタムレンダー

アプリのプロバイダーを一度ラップして再エクスポートすれば、各テストでネストを繰り返さずに済みます。

function customRender(
  ui: ReactElement,
  options?: Omit<RenderOptions, "wrapper">,
) {
  return render(ui, { wrapper: AllProviders, ...options });
}

export * from "@testing-library/react";
export { customRender as render }; // shadow the default export

オプションの型を拡張して、テストごとの初期状態(認証状態や開始時のルートなど)を受け入れられるようにします。そうすれば、テスト内で前提条件を設定できます。

完全なコード:examples/custom-render.md

パターン 6:アクセシブルな動作のアサーション

要素の検索だけでなく、アクセシビリティツリーを使って、キーボードのフォーカス順序、展開/選択状態、アナウンスをアサートします。

await user.tab();
expect(screen.getByRole("combobox")).toHaveFocus();

await user.keyboard("{Enter}");
expect(screen.getByRole("button", { name: /section 1/i })).toHaveAttribute(
  "aria-expanded",
  "true",
);

expect(await screen.findByRole("alert")).toHaveTextContent(
  /email is required/i,
);

完全なコード:examples/accessibility.md

パターン 7:within を使ったスコープ付きクエリ

within はクエリの範囲をコンテナーに限定します。これによって、繰り返し構造をテストできるようになります。

const bobRow = screen.getAllByRole("row")[2];
await user.click(within(bobRow).getByRole("button", { name: /edit/i }));

const billing = screen.getByRole("region", { name: /billing address/i });
await user.type(
  within(billing).getByLabelText(/street address/i),
  "123 Billing St",
);

完全なコード:examples/scoped-queries.md

パターン 8:userEvent のセットアップオプション

3つのオプションは動作を調整するのではなく変更するもので、いずれも setup() で設定します。

const user = userEvent.setup({
  advanceTimers: advanceTimersBy, // your runner's timer-advance fn; without it, fake timers hang
  delay: null, // skip the inter-event wait
  pointerEventsCheck: 0, // interact with an element whose CSS sets pointer-events: none
});

完全なコード:examples/configuration.md


<red_flags>

注意すべき問題

実行時に問題が発生します:

  • await なしの userEvent の呼び出し — アサーションは操作前の DOM に対して実行され、テストはタイミング次第で成功または失敗します
  • advanceTimers を setup() に渡さずにフェイクタイマーを設定する — userEvent 内部の遅延が解消されず、テストランナーのタイムアウトまでテストが停止します
  • setup() セッションではなく userEvent.click(...) を直接呼び出す — 直接呼び出す API は非推奨であり、セッションのポインターやキーボードの状態が一切反映されません
  • 非同期で表示される要素に getBy* を使う — 待機せず、すぐに例外が発生します
  • waitFor のコールバック内で副作用を起こす — コールバックはポーリングのたびに実行されるため、副作用が繰り返し発生します

予想外の動作:

  • container.querySelector(".btn-primary") は例外ではなく null を返すため、クエリ時ではなく、その後のアサーションで失敗します。また、リファクタリングで自由に変更できるクラス名やネスト構造を使って照合します。クエリ階層が避けようとしているのは、まさにこの点です
  • queryBy* は例外ではなく null を返します。そのため、要素が存在しないことのアサーションには適していますが、それ以外には適していません
  • findBy* のデフォルトのタイムアウトは 1000ms です。タイムアウトを延長するには、第 2 引数ではなく第 3 引数を使います
  • result.current は参照するたびに再取得されるため、ローカル変数に格納した値は次の act の後には古くなります
  • render、fireEvent、userEvent はすでに act でラップされています。さらにラップするのは冗長であり、本当に対処が必要な act 警告も見えにくくなります
  • フレームワークアダプターが登録されている環境ではクリーンアップが自動実行されるため、手動の afterEach(cleanup) は不要です
  • 1 つの waitFor に複数のアサーションを入れると、最初の失敗が明らかになるまでリトライ時間をすべて使います
  • 空の waitFor(() => {}) は次のポーリングのタイミングまで待つだけで、それ以外のことはしません。マッチャーの格好をした sleep です
  • 大きな DOM スナップショットは、原因ではなくノイズを含んだ失敗になります。スナップショットはアイコンやパンくずリストなど、小さくて安定した出力に使いましょう
  • screen.debug() は 7000 文字で切り詰められるため、フォームをデバッグすると途中の要素で表示が終わることがよくあります

コメント