12k
All articles

React Testing LibraryでDOMをクエリする方法

React Testing LibraryのgetBy、findBy、queryByを比較し、同期・非同期・条件付きDOM要素のコンポーネントテスト手法を解説する。

OpenReplay Team
OpenReplay Team
React Testing LibraryでDOMをクエリする方法

React Testing LibraryのDOMクエリは効果的なコンポーネントテストの基盤ですが、多くの開発者は特定のテストシナリオに適したクエリメソッドの選択に苦労しています。同期要素、非同期コンポーネント、条件付きレンダリングを扱う場合、getBy、findBy、queryByメソッドをいつ使用するかを理解することが、信頼性の高いテストと不安定なテストの違いを生み出します。

このガイドでは、React Testing Libraryの重要なクエリメソッド、その動作の違い、より堅牢なテストを書くための実践的な例を紹介します。クエリの優先順位ガイドライン、避けるべき一般的な落とし穴、テストの信頼性を向上させる実際のテストパターンを学習できます。

重要なポイント

  • レンダリング後すぐに存在すべき要素にはgetByクエリを使用する
  • API呼び出しや状態更新後など、非同期で表示される要素にはfindByクエリを使用する
  • 要素がDOMに存在しないことをテストする場合はqueryByクエリを使用する
  • より良いアクセシビリティテストのためにgetByRoleとgetByLabelTextクエリを優先する
  • セマンティッククエリが実用的でない複雑なシナリオではgetByTestIdを使用する
  • screen.debug()とTesting Playgroundを使用して失敗したクエリをデバッグし、より良いクエリ選択を行う

React Testing Library DOMクエリの開始

React Testing Libraryは、Reactコンポーネントのテスト専用に設計されたAPIを追加することで、DOM Testing Libraryの上に構築されています。Reactプロジェクトにインストールしてください:

npm install --save-dev @testing-library/react @testing-library/jest-dom

React Testing LibraryのDOMクエリは、ユーザーがアプリケーションと対話する方法と同様に、レンダリングされたコンポーネントのDOMツリー内の要素を見つけることで動作します。基本的な例を以下に示します:

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

test('renders login form', () => {
  render(<LoginForm />)
  const usernameInput = screen.getByLabelText('Username')
  expect(usernameInput).toBeInTheDocument()
})

DOM Testing Libraryとの主な違いは、React Testing Libraryがコンポーネントのレンダリング、状態更新、クリーンアップなどのReact固有の関心事を自動的に処理することです。

React Testing Libraryクエリタイプの理解

React Testing Libraryは3つの主要なクエリタイプを提供し、それぞれ欠落要素とタイミングの処理において異なる動作を示します。

getByクエリ - 同期要素選択

getByクエリは要素を即座に返し、要素が見つからない場合や複数の一致が存在する場合はエラーを投げます:

// 単一要素 - 見つからない場合や複数見つかった場合はエラーを投げる
const button = screen.getByRole('button', { name: 'Submit' })

// 複数要素 - 見つからない場合はエラーを投げる
const listItems = screen.getAllByRole('listitem')

レンダリング後すぐにDOM内に要素が存在することを期待する場合はgetByクエリを使用してください。

findByクエリ - 非同期DOMクエリ

findByクエリはPromiseを返し、要素が表示されるかタイムアウト(デフォルト1000ms)が発生するまで再試行します:

// 非同期要素の表示を待つ
const successMessage = await screen.findByText('Profile updated successfully')

// 複数の非同期要素
const loadedItems = await screen.findAllByTestId('product-card')

API呼び出し、状態更新、またはその他の非同期操作後に表示される要素にはfindByクエリを使用してください。

queryByクエリ - 条件付き要素テスト

queryByクエリは要素が見つからない場合にnullを返すため、要素の不在をテストするのに最適です:

// 要素が存在しないことをテスト
const errorMessage = screen.queryByText('Error occurred')
expect(errorMessage).not.toBeInTheDocument()

// 複数要素 - 見つからない場合は空配列を返す
const hiddenElements = screen.queryAllByTestId('hidden-item')
expect(hiddenElements).toHaveLength(0)
クエリタイプ0件の一致1件の一致>1件の一致非同期
getByエラーを投げる要素を返すエラーを投げるいいえ
queryBynullを返す要素を返すエラーを投げるいいえ
findByエラーを投げる要素を返すエラーを投げるはい

React Testing Libraryクエリ優先順位ガイド

React Testing Libraryは、ユーザーがコンポーネントと対話する方法でテストすることを推奨しています。この優先順位ガイドは、実際のユーザー行動を反映するクエリの選択に役立ちます。

アクセシビリティファーストクエリ(getByRole、getByLabelText)

getByRoleはほとんどの要素に対する最初の選択肢であるべきです:

// ボタン、リンク、フォームコントロール
const submitButton = screen.getByRole('button', { name: 'Create Account' })
const navigationLink = screen.getByRole('link', { name: 'About Us' })

// 特定レベルの見出し
const pageTitle = screen.getByRole('heading', { level: 1 })

getByLabelTextはフォームフィールドに最適です:

const emailInput = screen.getByLabelText('Email Address')
const passwordInput = screen.getByLabelText(/password/i)

これらのクエリは、コンポーネントが支援技術と連携することを保証します。

コンテンツベースクエリ(getByText、getByPlaceholderText)

getByTextは表示されるテキストコンテンツで要素を見つけます:

// 完全一致テキスト
const welcomeMessage = screen.getByText('Welcome back, John!')

// 柔軟なマッチングのための正規表現
const errorText = screen.getByText(/something went wrong/i)

getByPlaceholderTextはラベルが利用できない場合に役立ちます:

const searchInput = screen.getByPlaceholderText('Search products...')

getByTestIdを使用するタイミング

セマンティッククエリが十分でない場合にgetByTestIdを使用してください:

// テキストが変更される動的コンテンツ
const userAvatar = screen.getByTestId('user-avatar')

// 明確な役割のない複雑なコンポーネント
const chartContainer = screen.getByTestId('sales-chart')

テストIDは控えめに追加し、可能な限りセマンティッククエリを優先してください。

React Testing Library DOMクエリの一般的な落とし穴

適切なクエリ選択による不安定なテストの回避

問題: 非同期で読み込まれる要素にgetByクエリを使用する:

// ❌ 不安定 - 要素がまだ読み込まれていない可能性がある
test('shows user profile', () => {
  render(<UserProfile userId="123" />)
  const userName = screen.getByText('John Doe') // 失敗する可能性がある
})

解決策: 非同期要素にはfindByを使用する:

// ✅ 信頼性が高い - 要素が表示されるまで待つ
test('shows user profile', async () => {
  render(<UserProfile userId="123" />)
  const userName = await screen.findByText('John Doe')
  expect(userName).toBeInTheDocument()
})

失敗したクエリのデバッグ

クエリが失敗した場合、React Testing Libraryは有用なデバッグツールを提供します:

// 実際にDOMにあるものを確認
screen.debug()

// より良いクエリの提案を取得
screen.getByRole('button') // エラーメッセージで利用可能な役割を提案

実際のHTMLでクエリを試すにはTesting Playgroundを使用してください。

テストIDへの過度な依存

問題: getByTestIdをデフォルトのクエリメソッドとして使用する:

// ❌ ユーザー中心ではない
const button = screen.getByTestId('submit-button')

解決策: ユーザーの対話を反映するセマンティッククエリを使用する:

// ✅ ユーザー中心
const button = screen.getByRole('button', { name: 'Submit Form' })

テストIDは最初の選択肢ではなく、最後の手段であるべきです。

React Testing Libraryの実世界の例

異なるクエリメソッドの実際の使用例を以下に示します:

フォームテスト:

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

test('handles form submission', async () => {
  render(<ContactForm />)
  
  // フォームフィールドにはgetByLabelTextを使用
  const nameInput = screen.getByLabelText('Full Name')
  const emailInput = screen.getByLabelText('Email')
  const submitButton = screen.getByRole('button', { name: 'Send Message' })
  
  // フォームを入力して送信
  fireEvent.change(nameInput, { target: { value: 'John Doe' } })
  fireEvent.change(emailInput, { target: { value: 'john@example.com' } })
  fireEvent.click(submitButton)
  
  // 成功メッセージを待つ
  const successMessage = await screen.findByText('Message sent successfully!')
  expect(successMessage).toBeInTheDocument()
})

エラー状態テスト:

import { rest } from 'msw'

test('displays error when API fails', async () => {
  // API失敗をモック
  server.use(
    rest.get('/api/users', (req, res, ctx) => {
      return res(ctx.status(500))
    })
  )
  
  render(<UserList />)
  
  // エラーメッセージの表示を待つ
  const errorMessage = await screen.findByText(/failed to load users/i)
  expect(errorMessage).toBeInTheDocument()
  
  // ローディング状態が消えたことを確認
  const loadingSpinner = screen.queryByTestId('loading-spinner')
  expect(loadingSpinner).not.toBeInTheDocument()
})

まとめ

React Testing LibraryのDOMクエリをマスターするには、即座の要素にはgetBy、非同期コンテンツにはfindBy、要素の不在をテストするにはqueryByをいつ使用するかを理解する必要があります。getByRoleやgetByLabelTextなどのアクセシビリティ重視のクエリを優先し、セマンティッククエリが十分でないエッジケースではgetByTestIdを使用してください。

信頼性の高いテストの鍵は、ユーザーがコンポーネントとどのように対話するかを反映するクエリを選択することです。このアプローチにより、堅牢で保守可能なテストが作成され、実装の変更に対して耐性を保ちながら、実際のユーザー向けの問題をキャッチできます。

FAQ

getByクエリの代わりにfindByをいつ使用すべきですか?

API呼び出し、setTimeout、状態更新などの非同期操作後に表示される要素をテストする場合はfindByクエリを使用してください。findByクエリは要素が表示されるかタイムアウトに達するまで自動的に再試行し、不安定なテストを防ぎます。

React Testing LibraryがgetByTestIdの使用を避けることを推奨するのはなぜですか?

getByTestIdクエリはユーザーがアプリケーションと対話する方法を反映しません。ユーザーはテストIDを見ません - ボタンと対話し、テキストを読み、フォームラベルを使用します。getByRoleやgetByTextなどのセマンティッククエリは、より現実的で保守可能なテストを作成します。

要素がレンダリングされていないことをテストするにはどうすればよいですか?

要素が見つからない場合にエラーを投げる代わりにnullを返すqueryByクエリを使用してください。例:expect(screen.queryByText('Error message')).not.toBeInTheDocument()。

getAllByとfindAllByクエリの違いは何ですか?

getAllByクエリは要素の配列を即座に返し、要素が見つからない場合はエラーを投げます。findAllByクエリは配列に解決されるPromiseを返し、解決する前に少なくとも1つの要素が表示されるまで待機します。

React Testing Libraryクエリが失敗した場合、どのようにデバッグできますか?

screen.debug()を使用して現在のDOM構造を確認し、エラーメッセージでクエリの提案をチェックし、Testing Playgroundを試して実際のHTMLで異なるクエリアプローチを実験してください。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.