🐙
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が条件を満たしているかを判定する役目。
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 の使い分け
-
存在確認:
getBy+toBeInTheDocument() -
非存在確認:
queryBy+not.toBeInTheDocument() -
非同期:
findByまたはwaitFor -
複数要素:
getAllBy/queryAllBy/findAllBy
Selector の優先順位
-
ユーザー視点:
ByRole,ByLabelText,ByPlaceholderText,ByText -
セマンティック:
ByAltText,ByTitle -
最終手段:
ByTestId
頻出 Matcher
-
DOM 確認:
toBeInTheDocument(),not.toBeInTheDocument() -
状態確認:
toBeDisabled(),toBeEnabled(),toBeChecked(),toHaveValue() -
スタイル確認:
toHaveClass(),toHaveAttribute(),toHaveStyle() -
数値確認:
toHaveLength(),toBeGreaterThan()
まずはこの辺りのセットを押さえておけば、通常のテストでは困らないはずです。
あとは数をこなして、実務の中で少しずつ覚えていこうと思います。
Discussion