🐙

React Testing Library - Query と Matcher のまとめ

に公開

React Testing Library を仕事で初めて使うことになりました。
query (要素の取得方法) と matcher (検証方法) の理解を深めるため、使用頻度の高そうなものを整理しました。

React Testing Library のざっくり復習

  • React Testing Library とは React コンポーネントのテストライブラリ。ユーザーの視点でテストを書くことを重視している。
  • Query とは要素を取得する方法。screen.getByText() や screen.queryByRole() など。
  • Matcher とは expect(actual).matcher の matcher 部分。actual が条件を満たしているかを判定する役目。

https://testing-library.com/docs/react-testing-library/intro/

Query の種類(取得方法の違い)

要素を取得する際、3 つのバリエーションがあります:

Query タイプ 要素が見つからない場合 使いどころ 使用例
getBy エラーを投げる 要素が存在するはずの場合 screen.getByText('ログイン')
queryBy null を返す 要素が存在しないことを確認したい場合 screen.queryByText('エラー')
findBy Promise を返す 非同期で要素が表示されるのを待つ場合 await screen.findByText('完了')
getAllBy エラーを投げる 複数の要素が存在するはずの場合 screen.getAllByRole('button')
queryAllBy 空配列を返す 複数の要素が存在しないことを確認する場合 screen.queryAllByTestId('item')
findAllBy Promise を返す 非同期で複数の要素が表示されるのを待つ await screen.findAllByRole('listitem')

Selector の種類(何で要素を探すか)

要素を探す際の基準にも複数の種類があります。優先度が高い順に:

Selector 使いどころ 使用例 優先度
ByRole アクセシビリティを重視した取得(推奨) screen.getByRole('button', { name: 'ログイン' }) ⭐⭐⭐
ByLabelText フォーム要素をラベルで取得 screen.getByLabelText('メールアドレス') ⭐⭐⭐
ByPlaceholder プレースホルダーで取得 screen.getByPlaceholderText('例: user@example.com') ⭐⭐
ByText テキストコンテンツで取得 screen.getByText('ようこそ') ⭐⭐
ByDisplayValue フォームの現在値で取得 screen.getByDisplayValue('初期値') ⭐⭐
ByAltText 画像の alt 属性で取得 screen.getByAltText('プロフィール画像') ⭐⭐
ByTitle title 属性で取得 screen.getByTitle('詳細情報') ⭐
ByTestId data-testid 属性で取得(最終手段) screen.getByTestId('custom-element') ⭐

頻出する Matcher 一覧

DOM の存在確認

Matcher 使いどころ 使用例
toBeInTheDocument() 要素が DOM に存在するか expect(screen.getByText('ログイン')).toBeInTheDocument()
not.toBeInTheDocument() 要素が DOM に存在しないか expect(screen.queryByText('エラー')).not.toBeInTheDocument()

要素の状態確認

Matcher 使いどころ 使用例
toBeDisabled() 要素が無効化されているか expect(button).toBeDisabled()
toBeEnabled() 要素が有効化されているか expect(button).toBeEnabled()
toBeVisible() 要素が表示されているか expect(element).toBeVisible()
toBeChecked() チェックボックスがチェック済みか expect(checkbox).toBeChecked()
toHaveValue() フォーム要素が特定の値を持つか expect(input).toHaveValue('test@example.com')

スタイル・属性の確認

Matcher 使いどころ 使用例
toHaveClass() CSS クラスを持つか expect(button).toHaveClass('rounded-full')
toHaveAttribute() 属性を持つか expect(link).toHaveAttribute('href', '/home')
toHaveStyle() インラインスタイルを持つか expect(div).toHaveStyle({ display: 'none' })

数値・配列の確認

Matcher 使いどころ 使用例
toHaveLength() 配列の長さを確認 expect(items).toHaveLength(3)
toBeGreaterThan() 数値がより大きいか expect(items.length).toBeGreaterThan(1)
toBeLessThan() 数値がより小さいか expect(count).toBeLessThan(10)

真偽値の確認

Matcher 使いどころ 使用例
toBeTruthy() 真値かどうか expect(result).toBeTruthy()
toBeFalsy() 偽値かどうか expect(result).toBeFalsy()

非同期テストの書き方

非同期でレンダリングされる要素をテストする場合、waitFor や findBy を使います。

findBy を使う方法(推奨)

test('データ取得後にユーザー名が表示される', async () => {
  render(<UserProfile />)

  // findBy は Promise を返すので await が必要
  const username = await screen.findByText('山田太郎')
  expect(username).toBeInTheDocument()
})

waitFor を使う方法

test('複数の要素が非同期で表示される', async () => {
  render(<CourseList />)

  await waitFor(
    () => {
      expect(screen.queryByText('Web制作')).toBeInTheDocument()
      expect(screen.queryByText('Webデザイン')).toBeInTheDocument()
    },
    { timeout: 3000 } // タイムアウトを指定可能
  )
})

モックの基本

コンポーネントのモック

// Example コンポーネントをモックして、テストIDを持つシンプルな div に置き換え
vi.mock('@/components/Example', () => ({
  Example: () => <div data-testid="example" />
}))

カスタムフックのモック

// useFeature フックをモックして、固定の振る舞いを返す
vi.mock('@/modules/useFeature', () => ({
  useFeature: () => ({
    doSomething: () => {}
  })
}))

API のモック

// fetch API をモックして、特定のレスポンスを返す
const mockFetch = vi.fn().mockResolvedValue({
  ok: true,
  json: async () => ({ items: [] })
})
vi.stubGlobal('fetch', mockFetch)

// テスト実行...

// モックを元に戻す
vi.unstubAllGlobals()

実践的なテストパターン

パターン 1: 表示・非表示の確認

test('条件次第でメッセージが表示される', () => {
  render(<StatusMessage isActive />)

  // 表示されるべき要素
  expect(screen.getByText('表示されるテキスト')).toBeInTheDocument()

  // 表示されないべき要素(queryBy を使用)
  expect(screen.queryByText('非表示テキスト')).not.toBeInTheDocument()
})

パターン 2: 複数要素の数を確認

test('要素の数が期待通りに表示される', () => {
  render(<TodoList items={['買い物', '掃除', '読書']} />)

  const items = screen.getAllByRole('listitem')
  expect(items).toHaveLength(3)
})

パターン 3: Role を使ったアクセシブルなテスト

test('ボタンがレンダリングされる', () => {
  render(<Button>送信</Button>)

  // role と name でボタンを取得(アクセシビリティに配慮)
  const button = screen.getByRole('button', { name: '送信' })
  expect(button).toBeInTheDocument()
})

パターン 4: CSS クラスの確認

test('スタイル用のクラスが付与される', () => {
  render(<Button variant="square">スクエア</Button>)

  const button = screen.getByRole('button', { name: 'スクエア' })
  expect(button).toHaveClass('is-square')
})

まとめ

Query の使い分け

  1. 存在確認: getBy + toBeInTheDocument()
  2. 非存在確認: queryBy + not.toBeInTheDocument()
  3. 非同期: findBy または waitFor
  4. 複数要素: getAllBy / queryAllBy / findAllBy

Selector の優先順位

  1. ユーザー視点: ByRole, ByLabelText, ByPlaceholderText, ByText
  2. セマンティック: ByAltText, ByTitle
  3. 最終手段: ByTestId

頻出 Matcher

  1. DOM 確認: toBeInTheDocument(), not.toBeInTheDocument()
  2. 状態確認: toBeDisabled(), toBeEnabled(), toBeChecked(), toHaveValue()
  3. スタイル確認: toHaveClass(), toHaveAttribute(), toHaveStyle()
  4. 数値確認: toHaveLength(), toBeGreaterThan()

まずはこの辺りのセットを押さえておけば、通常のテストでは困らないはずです。
あとは数をこなして、実務の中で少しずつ覚えていこうと思います。

Discussion