---
title: "React Testing Libraryのuser-eventとクエリ実践｜getByRole・findBy・fireEventの使い分け"
url: "https://www.issoh.co.jp/tech/details/4465/"
published: 2024-12-10
updated: 2026-08-03
categories: ["React"]
publisher: "株式会社一創"
---

# React Testing Libraryのuser-eventとクエリ実践｜getByRole・findBy・fireEventの使い分け

React Testing Library（RTL）でテストが落ちる原因の多くは、ライブラリの思想ではなく**user-eventの呼び出し方とクエリの選び方**に集約されます。user-event は v14 で `setup()` と `await` が前提になり、v13 までの書き方をそのまま持ち込むと非同期処理が噛み合わずに失敗するのです。クエリ側も、`getByRole('textbox')` が拾える要素と拾えない要素の境目を知らないまま使うと、実装は正しいのにテストだけが落ちます。

この記事では user-event の API とクエリメソッドの選び分けだけを、公式ドキュメントと npm の実バージョンに当たって整理します。RTLの設計思想・インストール・ベストプラクティス全般は[React Testing Libraryとは？使い方・クエリ・ベストプラクティスを実務目線で解説](https://www.issoh.co.jp/tech/details/3521/)にまとめてあります。

## まとめ

user-event は v14 系で書き方が変わりました。`const user = userEvent.setup()` を各テストの先頭に置き、すべての操作に `await` を付けます。直接呼び出しは v14 でも動きますが、公式ドキュメントは v13 からの移行を楽にするための互換手段だと明記しています。

クエリは `getByRole` が第一候補です。ただしロールは見た目ではなく W3C の対応表で決まり、`input type="search"` は `searchbox`、`list` 属性が付けば `combobox`、`input type="password"` にはロールがありません。ロールで取れない入力欄は `getByLabelText` の担当です。非同期は `findBy` で待ち、要素が「無いこと」を確かめるときだけ `queryBy` を使います。

jest-dom は 2026年7月20日に 7.0.0 が出て `@testing-library/dom` が必須の peer dependency（`>=10 <11`）になり、Node.js 22 未満が切られました。`toBeInTheDocument` が動かないときは、まずこの2点を疑ってください。

## user-event v14のセットアップ｜setup()とawaitが前提になった書き方

user-event は RTL 本体（`@testing-library/react`）とは別パッケージです。npm レジストリで確認した最新は **14.6.1**（2026年8月時点）、peer dependency は `@testing-library/dom >=7.21.4` です。RTL 16.3.2 は `@testing-library/dom ^10.0.0` を peer に持つため、RTL と併用する限りこの条件は自然に満たされます。

```
npm install --save-dev @testing-library/user-event
```

### setup()を先に呼ぶ理由｜キーボードとポインタの状態保持

v14 では `userEvent.setup()` が返すインスタンス経由で操作します。setup したインスタンスはキーボードやポインタの状態（押しっぱなしの修飾キー、クリップボードの内容）をテスト内で保持するため、複数の操作をまたぐシナリオが正しく再現されます。

```
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import LoginForm from './LoginForm'

test('メールとパスワードを入力して送信できる', async () => {
  const user = userEvent.setup()
  render(<LoginForm />)

  await user.type(screen.getByLabelText('メールアドレス'), 'taro@example.com')
  await user.type(screen.getByLabelText('パスワード'), 'secret')
  await user.click(screen.getByRole('button', { name: '送信' }))

  expect(await screen.findByText('ログインしました')).toBeInTheDocument()
})
```

### v13スタイルからの移行手順｜置換すべき3点

公式ドキュメントは `userEvent.click()` のような直接呼び出しについて「v14 でも引き続きサポートされるが、これは v13 から v14 への移行を楽にするためと、単純なテストのための選択肢である」と述べています。動くには動きますが、v14 の API はすべて `Promise` を返すため、`await` を落とすと失敗の原因が追えなくなります。

移行作業そのものは機械的です。テスト冒頭に `const user = userEvent.setup()` を1行足す。`userEvent.xxx(` を `await user.xxx(` に置換する。テスト関数を `async` にする。この3点で v13 スタイルは解消できます。

### setup()の主なオプション｜フェイクタイマー併用時の必須設定

フェイクタイマーとの併用は事故が起きやすい箇所です。Jest の `jest.useFakeTimers()` や Vitest の `vi.useFakeTimers()` を有効にしたまま user-event を動かすと、内部の待機がタイマーに食われて進まなくなります。公式ドキュメントは `advanceTimers` について「フェイクタイマーを使う場合、このオプションにテストランナーの時間を進める関数を設定する必要がある」と明記しています。

| オプション              | 既定値                                 | 用途                         |
| ------------------ | ----------------------------------- | -------------------------- |
| delay              | 0                                   | キー入力ごとの待ち時間（ms）            |
| advanceTimers      | () => Promise.resolve()             | フェイクタイマー使用時に時間を進める関数       |
| pointerEventsCheck | PointerEventsCheckLevel.EachApiCall | pointer-events: none の検査頻度 |
| skipHover          | false                               | click前のhover動作を省く          |
| applyAccept        | true                                | upload時にaccept属性でファイルを絞る   |
| writeToClipboard   | 直接呼び出しはfalse／setup経由はtrue           | コピー操作をクリップボードへ反映           |

```
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime })
```

ここで踏んではいけない回避策があります。公式ドキュメントは「フェイクタイマー使用時のテストのタイムアウトを防ぐために `delay: null` を設定するという提案を見かけるかもしれないが、予期しない挙動を引き起こす可能性があるため推奨されない」と警告しています。タイムアウトが出たら `delay` を潰すのではなく、`advanceTimers` を正しく渡してください。

### pointerEventsCheckの調整｜遅いテストの切り分け

`pointerEventsCheck` は、ポインタ API が対象要素の `pointer-events: none` を継承していないかを検査する設定です。深くネストしたツリーでは検査コストが無視できないと公式に注意書きがあり、テストが遅いときは `Never`／`EachTarget`／`EachApiCall`／`EachTrigger` の4段階で下げられます。

検査対象はあくまで CSS の `pointer-events: none` であり、要素同士の重なりではありません。jsdom はレイアウトを持たないため「モーダルの下に隠れている」ことは原理的に検知されず、クリックはそのまま成功します。この検査に引っかかるのは、モーダルライブラリが背景要素に `pointer-events: none` を当てている構成だけです。

## fireEventとuser-eventの使い分け｜1回のクリックで起きることの差

迷ったら user-event を既定にします。理由は再現するイベント数の差です。`fireEvent.click(el)` は `click` イベントを1つ発火するだけですが、user-event の `click()` は内部的に `pointer([{target: element}, {keys: '[MouseLeft]', target: element}])` と等価で、ポインタの移動から左ボタンの押下までを順に再現します（最初の hover は `skipHover` で省略可能）。

この差が表面化するのは、hover でメニューが開く、focus で検証メッセージが出る、`pointerdown` でドラッグ開始を判定するといった実装です。fireEvent で書いたテストはこれらを素通りするため、ブラウザでは壊れているのにテストは緑という状態を作ります。

### fireEventを選ぶ場面｜ユーザー操作で起こせないイベント

fireEvent が適するのは、人間の操作としては自然に起こせないイベントを直接投げたいときです。`window` の `resize`、`scroll`、`animationend`、あるいはサードパーティ製ウィジェットが独自に発火するカスタムイベントが該当します。

```
import { fireEvent, screen } from '@testing-library/react'

fireEvent(window, new Event('resize'))
fireEvent.animationEnd(screen.getByTestId('toast'))
```

逆に、クリック・入力・選択・タブ移動といったマウスとキーボードで行う操作を fireEvent で書くのは避けてください。速度を理由に fireEvent へ寄せると、上に挙げた hover・focus・pointerdown 起因の不具合をすべて取りこぼします。

## クエリの選び分け｜getByRoleを第一候補にする実務基準

公式ドキュメントの優先順位は、`getByRole` → `getByLabelText` → `getByPlaceholderText` → `getByText` → `getByDisplayValue` → `getByAltText` → `getByTitle` → `getByTestId` の順です。上位ほど「支援技術を含む実際の利用者が要素を見つける方法」に近く、`getByTestId` はロールでもテキストでも一致させられない場合の最後の手段と位置づけられています。

### input type別のロール対応表｜textboxで取れない要素

`getByRole('textbox')` で入力欄が見つからない原因の多くは、この対応表にあります。ロールは W3C の「ARIA in HTML」仕様で要素と属性の組み合わせごとに決まっており、見た目が同じテキスト入力でも `type` と `list` 属性で別のロールになります。

| 要素                                                               | 暗黙のロール    |
| ---------------------------------------------------------------- | --------- |
| input type=”text” / “email” / “tel” / “url”（list属性なし）            | textbox   |
| input type=”search”（list属性なし）                                    | searchbox |
| input type=”text” / “search” / “tel” / “url” / “email”（list属性あり） | combobox  |
| input type=”password”                                            | 対応するロールなし |
| textarea                                                         | textbox   |

実務上のインパクトが大きいのは下2行です。パスワード入力欄には暗黙のロールが割り当てられないため、`getByRole` ではどう書いても取得できません。ここは `getByLabelText` を使う一手です。オートコンプリート候補を出すために `list` 属性を付けた検索欄も、`searchbox` ではなく `combobox` になる点に注意してください。

```
screen.getByLabelText('パスワード')
screen.getByRole('searchbox', { name: 'サイト内検索' })
screen.getByRole('combobox', { name: 'キーワード' })
```

### nameオプションの絞り込み｜文字列は完全一致・正規表現は部分一致

同じロールの要素が複数あると `getByRole` はエラーになります。`name` オプションにはアクセシブルな名前（ラベル、`aria-label`、ボタンの表示テキスト）を渡します。文字列を渡すと完全一致、正規表現を渡すと部分一致になるため、末尾に空白や記号が入りうる表示テキストには正規表現が安全です。

```
screen.getByRole('button', { name: '保存' })
screen.getByRole('button', { name: /保存/i })
screen.getByRole('heading', { name: '請求先', level: 2 })
```

### within()によるスコープ限定｜getQueriesForElementのエイリアス

一覧画面のように「削除」ボタンが行数ぶん並ぶ画面では、`name` だけでは絞り込めません。`within()` は公式ドキュメントで `getQueriesForElement` のエイリアスと説明されており、渡した DOM 要素にクエリを束縛して、その配下だけを検索対象にします。Storybook の play 関数で `within(canvasElement)` を `canvas` という名前で受けてから `canvas.getByRole('textbox')` と書くのも同じ仕組みです。

```
import { screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('該当行の削除ボタンだけを押す', async () => {
  const user = userEvent.setup()
  const row = screen.getByRole('row', { name: /田中太郎/ })
  await user.click(within(row).getByRole('button', { name: '削除' }))
})
```

## getBy・queryBy・findByの使い分け｜同期と非同期の境目

接頭辞は3種類あり、「見つからなかったとき」の挙動で選びます。取り違えると、待つべき場面で待たずに落ちたり、無いことを確かめたい場面で例外になったりします。

| 接頭辞         | 0件      | 1件    | 複数件   | 非同期 |
| ----------- | ------- | ----- | ----- | --- |
| getBy…      | 例外      | 要素を返す | 例外    | いいえ |
| queryBy…    | nullを返す | 要素を返す | 例外    | いいえ |
| findBy…     | 例外      | 要素を返す | 例外    | はい  |
| getAllBy…   | 例外      | 配列を返す | 配列を返す | いいえ |
| queryAllBy… | 空配列を返す  | 配列を返す | 配列を返す | いいえ |
| findAllBy…  | 例外      | 配列を返す | 配列を返す | はい  |

### findByの待ち方｜既定タイムアウトは1000ms

`findBy` 系は `getBy` と `waitFor` を組み合わせたもので、公式ドキュメントの既定タイムアウトは 1000ms です。API 応答を待つ表示の検証はこれで足ります。

```
expect(await screen.findByText('保存しました')).toBeInTheDocument()
expect(await screen.findByRole('alert')).toHaveTextContent('通信に失敗しました')
```

1000ms で足りない処理を待つ場合は、タイムアウトを延ばす前に何を待っているかを見直してください。ネットワークを直接叩いているならモックへ置き換えるのが先です（[モックとは？スタブとの違い・テストダブル5分類と単体テストでの使い分けを実装者向けに解説](https://www.issoh.co.jp/tech/details/13505/)で分類を整理しています）。それでも延ばす必要があれば第3引数のオプションで指定します。

```
await screen.findByText('集計完了', {}, { timeout: 3000 })
```

### 「表示されないこと」の検証｜queryByと消滅待ち

要素が存在しないことを確かめたいときに `getBy` を使うと、0件で例外になるためテスト自体が落ちます。`queryBy` は `null` を返すので、こちらを使います。

```
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
```

消えるまで待つ場合は `waitForElementToBeRemoved` を使います。`findBy` には「無くなるまで待つ」用途がないため、ここだけは別APIになる点に注意してください。

## user-eventの主要API｜入力・選択・ファイル操作

クリックと入力以外の操作は、専用APIを知っているかどうかで書きやすさが変わります。この章で扱うAPIはいずれも `Promise<void>` を返すので `await` が必要です。

### typeとkeyboardの書き分け｜特殊キーの記法

`type()` は対象要素に文字列を打ち込む操作、`keyboard()` はフォーカス中の要素にキー入力そのものを送る操作です。印字可能な文字はそのまま並べ、特殊キーは `KeyboardEvent.key` なら波括弧、`KeyboardEvent.code` なら角括弧で囲みます。

```
await user.type(screen.getByRole('textbox'), 'issoh')
await user.keyboard('{Shift>}A{/Shift}')
await user.keyboard('[ShiftLeft][KeyF][KeyO][KeyO]')
```

1行目の `{Shift>}A{/Shift}` は Shift の押下、A の入力、Shift の解放という順序に展開されます。`>` が押しっぱなし、`/` が解放を意味する記法です。一方、2行目の `[ShiftLeft]` は押下と解放が即座に完結するため、入力結果は大文字ではなく `foo` になります。大文字にしたいなら `[ShiftLeft>]foo[/ShiftLeft]` と押しっぱなしの記法を使ってください。開き括弧そのものを入力したい場合は `{{` や `[[` のように二重にします。

### clear・selectOptions・uploadの制約｜reject条件とmultiple・accept

`clear()` は要素にフォーカスして全選択・削除する操作です。公式ドキュメントには、フォーカスできない要素や内容を選択できない要素では Promise が reject されると書かれています。読み取り専用の入力欄に対して呼ぶと失敗するのはこの仕様によるものです。

```
const file = new File(['dummy'], 'receipt.png', { type: 'image/png' })

await user.clear(screen.getByLabelText('備考'))
await user.selectOptions(screen.getByRole('combobox', { name: '都道府県' }), '東京都')
await user.upload(screen.getByLabelText('添付ファイル'), file)
```

`selectOptions()` は値・表示テキスト・要素そのもののいずれでも指定できますが、複数選択と選択解除は `select` 要素に `multiple` が付いている場合のみ可能です。`upload()` は `accept` 属性に一致しないファイルを自動的に破棄します。この挙動は `applyAccept: false` で無効化できるため、「拒否されるファイルを渡したときのエラー表示」を検証したい場合は setup 時に切ってください。

### tab()によるフォーカス順の検証｜shift:trueで逆方向

`tab({ shift: true })` で逆方向のフォーカス移動を再現できます。フォーカストラップやフォームの入力順は、実装ミスが多いわりにクリック中心のテストでは検出できない領域です。[Testing Trophy（テスティングトロフィー）とは？テストピラミッドとの違いと理想のテスト配分を解説](https://www.issoh.co.jp/tech/details/8765/)が示す配分でいえば、この種の検証こそコンポーネントテストで拾うべき部分にあたります。

```
await user.tab()
await user.tab({ shift: true })
expect(screen.getByLabelText('メールアドレス')).toHaveFocus()
```

## jest-dom 7.0.0の破壊的変更｜toBeInTheDocumentが動かないときの確認点

`toBeInTheDocument` や `toHaveFocus` は RTL ではなく `@testing-library/jest-dom` が提供するマッチャです。このパッケージは **2026年7月20日に 7.0.0** が公開されました。

### 7.0.0で壊れる構成｜dom 9系とNode 21以下

リリースノートに記載された破壊的変更は次の2点です。

- `@testing-library/dom` が必須の peer dependency になった（実際の許容範囲は `>=10 <11`）
- サポートする Node.js の最小バージョンが 22 になった

あわせて `toContainAnyBy*` と `toContainOneBy*` のクエリ系マッチャが追加されています。実害が出るのは、`@testing-library/dom` が 9系以下で止まっている構成（RTL 15系から据え置きのプロジェクト）と、CI の Node.js が 21 以下の構成です。RTL 16.3.2 は `@testing-library/dom ^10.0.0` を peer に持つため、16 系に上げてあれば範囲は自動的に満たされます。pnpm などで peer の自動解決を切っている場合だけ、明示的な devDependency の追加が要ります。

### マッチャの読み込み｜エントリポイントの選択

読み込み方法には古い情報が残りがちです。`@testing-library/jest-dom/extend-expect` というエントリポイントは 6.0.0 で削除されました。公式Readmeには元から記載が無く、解説記事を経由して広まった書き方だったため、いまだにこの記述を残したプロジェクトが見つかります。現在はパッケージ本体、または実行環境に応じた `/vitest`・`/jest-globals`・`/matchers` を読み込みます。

```
// setupTests.ts
import '@testing-library/jest-dom'
```

```
// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/setupTests.ts'],
}
```

この設定を動かすには、Jest 28 以降で別パッケージになった `jest-environment-jsdom` の追加と、TypeScript の setup ファイルを読むための transform 設定が必要です。Jest 側の環境構築は[ハブ記事のインストール章](https://www.issoh.co.jp/tech/details/3521/)、Vitest を使う場合は[Vitestの使い方｜React Testing Libraryでコンポーネントテストを書く実践ガイド【2026年版】](https://www.issoh.co.jp/tech/details/2867/)に手順があります。依存を上げる際は[テストカバレッジとは？C0/C1/C2の網羅率と計測ツール・目標設定を実装者向けに解説](https://www.issoh.co.jp/tech/details/13507/)で扱う計測設定も同時に確認しておくと手戻りが減ります。

## よくある質問（FAQ）

### `@testing-library/user-event` はどのバージョンを入れればよいですか？

2026年8月時点の npm 最新は 14.6.1 です。peer dependency は `@testing-library/dom >=7.21.4` で、RTL 16.3.2 と併用する構成なら追加の調整は不要です。13系が残っている場合は、`setup()` の導入と `await` の付与だけで14系へ移行できます。

### `userEvent.click()` に await を付けないと何が起きますか？

v14 の API はすべて `Promise` を返すため、`await` がないと操作の完了を待たずに次の行へ進みます。クリックで発生する再レンダリングや状態更新が終わる前にアサーションが評価され、要素が見つからないという実装とは無関係な失敗になります。エラーメッセージが原因を指さないので、user-event を使うテストは関数を `async` にすることを既定にしてください。

### `getByRole('textbox')` で入力欄が見つからないのはなぜですか？

暗黙のロールが `textbox` ではない可能性が高いです。W3C の「ARIA in HTML」では `input type="search"`（`list` 属性なし）は `searchbox`、`list` 属性が付いた text・search・tel・url・email は `combobox` になり、`input type="password"` には対応するロールがありません。`type` と `list` 属性を確認し、ロールが割り当たらない要素は `getByLabelText` で取得します。

### `findByText` と `waitFor` はどう使い分けますか？

要素の出現を待つだけなら `findByText` を使います。`waitFor` は「要素以外の条件（モック関数が呼ばれた、URLが変わった）を待つ」場合に限って使い、1つの `waitFor` に複数のアサーションを詰め込まないでください。どれが失敗したのか特定できなくなります。

### `screen.getByLabelText` でラベルが見つからないと言われるのはなぜですか？

ラベルと入力欄の関連付けが成立していないケースがほとんどです。`label` の `for` 属性と入力欄の `id` が一致しているか、`label` が入力欄を入れ子にしているかを確認してください。この2つ以外に `aria-label`、`aria-labelledby`、`title` 属性からも名前は取得できます。テストを直す前にマークアップ側を疑うのが近道です。

## 関連記事

- [React Testing Libraryとは？使い方・クエリ・ベストプラクティスを実務目線で解説](https://www.issoh.co.jp/tech/details/3521/)
- [Vitestの使い方｜React Testing Libraryでコンポーネントテストを書く実践ガイド【2026年版】](https://www.issoh.co.jp/tech/details/2867/)
- [モックとは？スタブとの違い・テストダブル5分類と単体テストでの使い分けを実装者向けに解説](https://www.issoh.co.jp/tech/details/13505/)
- [Testing Trophy（テスティングトロフィー）とは？テストピラミッドとの違いと理想のテスト配分を解説](https://www.issoh.co.jp/tech/details/8765/)
- [テストカバレッジとは？C0/C1/C2の網羅率と計測ツール・目標設定を実装者向けに解説](https://www.issoh.co.jp/tech/details/13507/)

---

出典: [React Testing Libraryのuser-eventとクエリ実践｜getByRole・findBy・fireEventの使い分け](<https://www.issoh.co.jp/tech/details/4465/>)（株式会社一創）
