react-component-testing について
react-component-testingは、Vite、React、TypeScriptプロジェクト向けのコンポーネントテストスキルです。RTL、MSW、vitest-axeを組み合わせ、ネットワーク境界のモックとユーザーに見える動作、実行時のアクセシビリティを検証します。
主な機能
- 01jsdomのテスト環境設定
- 02RTLによるコンポーネントの描画と検索
- 03user-eventによる操作の再現
- 04MSWによるネットワークリクエストの捕捉
- 05vitest-axeによるアクセシビリティ検査
- 06TanStack Queryの状態とフォームのテスト
活用シーン
- 01Reactコンポーネントの操作テスト作成
- 02RTL、MSW、アクセシビリティテストの設定
- 03リクエストをモックしたフォーム送信の検証
- 04クエリの読み込み、成功、エラー状態のテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bm629/ agent-skills --skill react-component-testingインストール後、Claude Code で/react-component-testingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/accessibility-with-vitest-axe.mdbrowser-mode.mdmsw-and-generated-clients.mdsources.mdstates-and-forms.md詳細
react-component-testing — SKILL.md
バリアント: 標準 · 使用するタイミング: スキルが呼び出されると、セットアップと作成済みのコンポーネントテスト層(環境 + setupFiles + 実際に動作するテスト)を完了まで実行して返し、制御を呼び出し元に戻します。
forge で検証済みのバージョン。
@testing-library/react16.x ·@testing-library/user-event14.x ·@testing-library/jest-dom6.x ·msw2.x ·vitest-axe0.1.x(axe-coreをラップ)·@tanstack/react-query5.x ·vitest3.x · jsdom。バージョンに依存しない形で説明し、利用時にはメンテナーのドキュメントで正確なメジャーバージョンと happy-dom の注意点を再確認してください(references/sources.mdを参照)。
概要
コンポーネントテストは、テストピラミッドの中間層です。Vitest のユニットテスト(純粋なロジック — ランナーは vitest スキルが担当)より上、Playwright の e2e(playwright-best-practices スキル)より下に位置します。実際のコンポーネントツリーをレンダリングし、ユーザーに見えるものやユーザーが行う操作を検証します。これにより、ユニットテストでは見逃されるコンポーネント + フック + データ取得間の連携を検出できます。また、すべての状態をブラウザー e2e で操作するより、はるかに低コストです。3 つのツールを組み合わせて、一貫した 1 つの層を構成します。**React Testing Library(RTL)**は、ユーザーの認識に沿った方法でツリーをレンダリングして検索し、user-event で操作します。Mock Service Worker(MSW v2)はネットワーク境界をモックし、アプリの実際の HTTP クライアントとシリアライズをエンドツーエンドで実行します。vitest-axeは、レンダリングされた出力に検出可能なアクセシビリティ違反がないことを検証します。重要な原則は、ソフトウェアの使われ方にテストを近づけるほど、テストからより大きな信頼を得られるということです。そのため、ロールやラベルで検索し、ユーザーのように操作し、モジュールではなくネットワーク境界でモックし、実装の詳細ではなく観測可能な振る舞いを検証します。このスキルが担当するのは、RTL + MSW + vitest-axe によるテスト作成層と、コンポーネントテスト環境(setupFiles)です。ランナー、ルーターハーネス、静的 a11y リントについては参照するだけで、再説明はしません。
有効にするタイミング
- ✅ Vite + React + TS の SPA でコンポーネントテストを作成する場合:レンダリング → 操作 → 状態の検証 → アクセシビリティの検証。
- ✅ コンポーネントテスト環境をセットアップする場合:
jsdom環境、@vitejs/plugin-react、setupFiles(jest-dom マッチャー、RTL のクリーンアップ、MSW サーバーのライフサイクル、vitest-axe の拡張)。 - ✅ テストで HTTP/データ取得境界をモックする場合。特に、生成された
@hey-api/openapi-ts(またはopenapi-fetch)クライアントを使い、実際のクライアントを実行して契約のずれを検出する場合。 - ✅ モックしたネットワークレスポンスを切り替えて、TanStack Query の読み込み中/空/成功/エラー状態をテストする場合。
- ✅ フォームをテストする場合:
user-eventで入力し、MSW ハンドラーが取得したリクエストボディを検証し、toHaveNoViolationsを検証する場合。 - ✅ 既存のコンポーネントテストに、実行時のアクセシビリティ検証を追加する場合。
次の場合は有効にしないでください:
- 作業対象が Vitest のランナー自体(設定、カバレッジのしきい値、watch/CI)の場合。担当するのは
vitestスキルであり、このスキルは参照のみ行います。 - 作業対象がブラウザーの e2e テストの場合。Playwright +
@axe-core/playwrightはplaywright-best-practicesが担当します。(Vitest のブラウザーモードは、デフォルトではなく、ドキュメントに記載された代替手段としてreferences/browser-mode.mdで説明されています。) - 作業対象が静的 a11y リントの場合。
biomeスキル(jsx-a11y ルールを移植したもの)がリント時の a11y を担当し、vitest-axe は実行時の補完です。 - 作業対象がルーターのテストハーネスの場合。ルーティングされたコンポーネントのレンダリングには
createMemoryHistory+ テスト用RouterProviderが必要で、担当するのはtanstack-routerスキルです。このスキルはそれを参照します。 - 作業対象が React、TanStack Query、または shadcn のチュートリアルの場合。それぞれに専用のスキルがあり、このスキルはそれらと組み合わせて使用します。
ワークフロー
ステップ 1:ネットワーク境界の原則を理解する(基盤となる判断)
モジュール境界ではなく、MSW を使ってネットワーク境界をモックします。 コンポーネントがネットワークと通信する場合は、MSW で HTTP リクエストをインターセプトし、アプリの実際の生成済みクライアント、クエリフック、リクエスト/レスポンスのシリアライズをすべて実行します。これにより、契約の変更(フィールド名の変更、必須ボディキーの追加など)がテストで明らかになります。クライアントをモジュールモック(vi.mock('./generated-client'))すると、決め打ちのオブジェクトが返され、テスト対象のまさにそのレイヤーをスキップしてしまうため、契約のずれを検出できません。
ルール: ネットワークを通過するものはすべて MSW でモックし、vi.mock は HTTP 以外のコラボレーターのみに使用します。対象は、時計(vi.useFakeTimers)、ランダム ID/UUID 生成器、非決定的なブラウザー API などです。詳細と生成済みクライアントをインターセプトする方法については、references/msw-and-generated-clients.md を参照してください。
ステップ 2:コンポーネントテスト環境を設定する(jsdom を使用し、happy-dom は使用しない)
vitest.config.ts に、@vitejs/plugin-react を追加し、test.environment: 'jsdom'、test.globals: true(これにより RTL の自動クリーンアップと jest-dom のマッチャーが正しく登録されます。設定しない場合は { describe, it, expect, afterEach } を明示的にインポートし、afterEach で cleanup() を呼び出します)を設定して、test.setupFiles にセットアップモジュールを指定します。
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup-tests.ts'],
},
})
// src/test/setup-tests.ts
import '@testing-library/jest-dom/vitest' // toBeInTheDocument, toHaveValue, ...
import 'vitest-axe/extend-expect' // toHaveNoViolations
import { afterAll, afterEach, beforeAll } from 'vitest'
import { server } from './msw-server' // setupServer(...) — see references/msw-and-generated-clients.md
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
vitest-axe を実行するテストでは、happy-dom ではなく jsdom を使用してください。 Happy DOM には、axe-core の走査を妨げる Node.prototype.isConnected の動作が記録されているため、happy-dom では vitest-axe のアサーションが機能しません。jsdom は高速で、CI の負荷も軽くなります。(この注意点は、最新のドキュメントで改めて確認してください。references/sources.md を参照してください。アクセシビリティ以外のテストで happy-dom を使う必要がある場合は、axe テストを jsdom プロジェクトに配置してください。)globals: true を設定すると、RTL はテスト間で DOM を自動的にアンマウントしてクリーンアップします。globals を設定しない場合は、afterEach(cleanup) を追加してください。
ステップ 3:ユーザーが UI を認識する方法に沿ってレンダリングし、クエリする
render(<Component />) とグローバルな screen を使用します。アクセシブルクエリの優先順位に従ってクエリします。上位のものほど優先します。
getByRole(アクセシブルなnameを指定します。例:screen.getByRole('button', { name: /save/i })) — 支援技術とユーザーが認識する方法であり、最初に選ぶクエリです。getByLabelText(フォームフィールド)、次にgetByPlaceholderText。getByText(操作対象ではないテキスト)、次にgetByDisplayValue、getByAltText、getByTitle。getByTestId— 最後の手段としてのみ使用し、ユーザーに見える情報でノードを識別できない場合に限ります。
実装ではなく動作を検証します。画面上に表示される内容やユーザーができる操作を確認し、コンポーネントの状態、プロパティ名、内部の呼び出し回数は確認しません。getBy* は対象がない場合に例外をスローします(「必ず存在する」場合に使用)。queryBy* は null を返します(「存在してはならない」場合に使用)。findBy* は非同期です(次のステップを参照)。
ステップ 4:user-event v14 で操作を実行する — 常に await を付ける
テストごとに userEvent.setup() でセッションを一度作成し、すべての操作に await を付けます。v14 ではすべての操作が Promise を返すため、await がないと気づかないうちに競合が発生します。低レベルの fireEvent よりも user-event を優先してください。fireEvent は単一の生イベントをディスパッチする一方、user-event は実際のユーザーが行う一連のイベント(フォーカス、keydown、input、keyup、…)をシミュレートします。
const user = userEvent.setup()
await user.type(screen.getByLabelText(/email/i), '[email protected]')
await user.click(screen.getByRole('button', { name: /submit/i }))
ステップ 5:findBy*/waitFor で非同期の出力を処理する
フェッチを発生させる操作の後は、結果を待ちます。findBy* を優先してください。これは getBy* + waitFor をまとめたもので、要素が表示されるかタイムアウトするまで再試行します。例:expect(await screen.findByText(/saved/i)).toBeInTheDocument()。要素をクエリする方法がない場合に限り、単独の waitFor を使用します。その場合は、コールバック内に アサーションのみを記述してください。waitFor はコールバックを繰り返し実行するため、waitFor 内で 副作用(user-event、状態の変更など)を行わないでください。スピナーやローディングノードが消えることを検証するには、waitForElementToBeRemoved を使用します。
ステップ 6:カスタム render でアプリのコンテキストを提供する(TanStack Query、テーマなど)
ほとんどのコンポーネントにはプロバイダーが必要です。RTL を再エクスポートし、retry: false を指定した**レンダーごとに新しい QueryClient**をマウントするカスタム render を備えた test-utils ラッパーを作成します(バックオフ付きで再試行するのではなく、エラー状態がただちに確定的に解決されます)。
// src/test/test-utils.tsx
import { type ReactElement, type ReactNode } from 'react'
import { render, type RenderOptions } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
function makeWrapper() {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={client}>{children}</QueryClientProvider>
)
}
export function renderWithProviders(ui: ReactElement, options?: RenderOptions) {
return render(ui, { wrapper: makeWrapper(), ...options })
}
export * from '@testing-library/react'
レンダーごとに新しいクライアントを使うこと(および afterEach で server.resetHandlers() を呼び出すこと)で、テスト間のキャッシュ汚染を防げます。ルーティングされるコンポーネントの場合は、tanstack-router スキルのテスト用 RouterProvider と createMemoryHistory をこのラッパーに組み込みます。このハーネスはそのスキルが管理するため、ここで再構築しないでください。テストごとに MSW のレスポンスを変えて、ローディング/空({ data: [] })/成功/エラー(500/ProblemDetail)の各状態をテストします。詳しくは references/states-and-forms.md を参照してください。
ステップ 7:vitest-axe でアクセシビリティを検証する
レンダー後(およびチェックする状態になった後)に、レンダーされたコンテナーに対して axe を実行し、違反がないことをアサートします。マッチャーはセットアップファイル内の import 'vitest-axe/extend-expect'(または vitest-axe/matchers からの expect.extend(matchers))で登録されます。
import { axe } from 'vitest-axe'
const { container } = renderWithProviders(<SignupForm />)
expect(await axe(container)).toHaveNoViolations()
vitest-axe には実際の DOM が必要なため、jsdom を使います(ステップ 2)。適用範囲を正直に示すと、自動 axe-core が検出するのは WCAG の問題の一部(よく引用される割合はおよそ 3 分の 1 から半分)であり、手動レビューや支援技術を使ったレビューを補完するものであって、決して代替するものではありません。詳細とルール設定については references/accessibility-with-vitest-axe.md を参照してください。
ステップ 8:実例 — フォーム → user-event → リクエストボディをアサートする MSW → 状態 → a11y 違反なし
// src/features/signup/SignupForm.test.tsx (colocated *.test.tsx)
import { http, HttpResponse } from 'msw'
import { axe } from 'vitest-axe'
import { renderWithProviders, screen } from '../../test/test-utils'
import { server } from '../../test/msw-server'
import userEvent from '@testing-library/user-event'
import { SignupForm } from './SignupForm'
it('submits the form and shows success', async () => {
const user = userEvent.setup()
let captured: unknown
server.use(
http.post('/api/users', async ({ request }) => {
captured = await request.json() // assert the REAL request body
return HttpResponse.json({ id: 'u1' }, { status: 201 })
}),
)
const { container } = renderWithProviders(<SignupForm />)
await user.type(screen.getByLabelText(/email/i), '[email protected]')
await user.type(screen.getByLabelText(/^password/i), 's3cret!')
await user.click(screen.getByRole('button', { name: /create account/i }))
expect(await screen.findByText(/account created/i)).toBeInTheDocument()
expect(captured).toEqual({ email: '[email protected]', password: 's3cret!' })
// security invariant: a field that must never leave the form is absent both places
expect(screen.queryByLabelText(/internal token/i)).not.toBeInTheDocument()
expect(captured).not.toHaveProperty('internalToken')
expect(await axe(container)).toHaveNoViolations()
})
この例ではレイヤー全体をテストします。実際のクライアントがボディをシリアライズし、MSW がネットワーク境界でそれをキャプチャし、成功状態がレンダーされ、出力が a11y の観点からチェックされます。エラー/バリデーションのバリエーションとセキュリティ不変条件のパターンについては、references/states-and-forms.md を参照してください。
ステップ 9:規約と CI
コンポーネントテストは、コンポーネントの隣に *.test.tsx として配置します。test-utils.tsx、setup-tests.ts、msw-server.ts は src/test/ フォルダーにまとめます。カバレッジ、しきい値、ウォッチ/CI、ランナー設定は vitest スキルが管理します。そのスキルの test スクリプトと CI ゲートを使ってテストスイートを実行してください。このスキルではランナーについて再説明しません。
ルール
必須ルール(絶対に違反しないこと):
- MSW でネットワーク境界をモックし、生成された HTTP クライアントをモジュールモックしないでください。 モジュールモックではクライアントとシリアライズがスキップされ、契約のずれを検出できません。
vi.mockは HTTP 以外の連携先にのみ使用します。 user-eventの操作はすべてawaitします。 v14 では各操作が Promise を返します。awaitがないと、気付かないうちに競合が起き、テストが不安定になったり、誤って成功したりします。- vitest-axe のテストでは
happy-domではなくjsdomを使います。 Happy DOM のisConnectedの挙動により axe の走査が壊れるため、vitest-axe はその環境では動作しません。 - HTTP クライアントを作成する前に
server.listen()を実行します。 MSW はlisten()でインターセプターを設定するため、それより前にキャプチャされたクライアント/fetch はモックを迂回します(参考資料を参照)。 - まずアクセシビリティに基づいてクエリします(
getByRole/getByLabelText)。getByTestIdは最後の手段です。実際に確認できる振る舞いをテストし、実装の詳細はテストしません。 waitForコールバック内に入れるのはアサーションだけです。副作用は入れません。手動のwaitForよりfindBy*を優先します。- レンダーごとに新しい
QueryClientをretry: falseで使用し、テストごとにresetHandlers()を呼び出します。クライアントの共有や、テスト間のキャッシュ/ハンドラーの汚染を防ぎます。
推奨事項(必要に応じて変更可):
- クエリ対象の要素がある場合は、
waitForよりfindBy*を優先します。 - 非推奨の直接的な
userEvent.click(...)呼び出しより、テストごとに 1 回のuserEvent.setup()を優先します。 server.listen()ではonUnhandledRequest: 'error'を指定し、モックされていないリクエストが実ネットワークに到達するのではなく、明確に失敗するようにします。- a11y のアサーションは初期レンダーだけでなく、実際に提供する状態(ロード後、エラー後)に対して行います。
注意点
user-eventの呼び出しでawaitを忘れる。 アサーションが実行される前にインタラクションの Promise が解決されず、テストは誤った理由で成功するか、不安定になります。必ずawaitを付けてください。- happy-dom と axe の併用は、気付かないまま壊れます。 テストは「実行」されますが、
Node.prototype.isConnectedのバグにより vitest-axe が正しく動作しません。a11y テストではjsdomを指定してください。 listen()の前にクライアントをインポート時に作成する。 モジュールの読み込み時にfetchをインスタンス化する生成クライアント(トップレベルのconst client = createClient()など)は、MSW がパッチを適用する前のfetch参照をバインドすることがあります。MSW のセットアップファイルがsetupFilesに含まれていることを確認してください(最初に実行されます)。また、一部の生成クライアントやopenapi-fetchクライアントでは、MSW から見えるfetchを明示的に渡してください。詳しくはreferences/msw-and-generated-clients.mdを参照してください。- ハンドラーのパスにクエリパラメーターを含める。 MSW はパス名で照合します。
http.get('/api/x?foo=bar', ...)に?foo=barを含めても一致しません。代わりに、ハンドラー内でnew URL(request.url)からクエリパラメーターを取得してください。 waitFor内で副作用を起こす。waitForはコールバックを何度も再実行します。その中でuser-eventや状態の変更を行うと、繰り返し実行されます。アサーションのみにしてください。- jest-dom の
/vitestエントリーを忘れる。@testing-library/jest-dom/vitestではなく Jest 用のエントリーである@testing-library/jest-domをインポートすると、Vitest でマッチャーが登録されません。 - テスト間で古い QueryClient を使い回す。 1 つのクライアントを再利用すると最初のレスポンスがキャッシュされ、後続のテストで古いデータが使われます。レンダリングごとに新しいクライアントを作成してください。
- axe を過信する。 違反がゼロであることは必要ですが、それだけでは十分ではありません。axe がカバーするのは WCAG の一部であり、手動での確認や支援技術によるレビューも必要です。
アンチパターン
- 「API クライアントを
vi.mockすれば簡単です。」 それではテスト対象のレイヤーが削除されます。生成クライアントもシリアライズも実行されないため、コントラクトのずれを検出できません。MSW でネットワークをモックしてください。 - 「どこでも
getByTestIdを使えば安定します。」 テスト ID はアクセシビリティツリーを迂回するため、アクセスできない UI でもテストを通過してしまいます。ロールやラベルでクエリしてください。testidは最後の手段です。 - 「
fireEvent.clickで問題ありません。」 実際の操作シーケンスではなく、合成イベントを 1 つだけ送出します。ユーザーが実際に行う操作にはuser-eventを使ってください。 - 「happy-dom は高速なので、a11y テストにも使いましょう。」 axe が動作しなくなります。速度の向上は、a11y アサーションが気付かないまま機能しなくなるリスクに見合いません。
- 「コンポーネントが
fetchを呼び出したことや、フックの内部状態をアサートする。」 実装に密結合すると、動作に変化がなくてもリファクタリングでテストが壊れます。ユーザーに見える内容と、サーバーが受け取ったリクエストをアサートしてください。 - 「ここで Vitest の設定やカバレッジを改めて説明する。」 対象範囲外です。ランナーは
vitestスキルの担当なので、そちらを参照してください。
出力
Vite + React + TS の SPA で動作するコンポーネントテストレイヤー。@vitejs/plugin-react を使用する jsdom の Vitest 環境、jest-dom のマッチャー + RTL のクリーンアップ + MSW サーバーのライフサイクル + vitest-axe の拡張を設定する setupFiles モジュール、新しい TanStack Query クライアントと retry:false を備え、ルーターハーネスと組み合わせて使える test-utils のカスタム render、そしてコンポーネントをレンダリングして user-event で操作し、生成クライアントを含むネットワークを MSW でモックし、リクエストボディと UI の状態、および toHaveNoViolations をアサートする、作成済みの *.test.tsx ファイルが含まれます。これを利用するのは次のワークフローフェーズです。つまり、ランナー/カバレッジ/CI を vitest スキルから提供された状態で、コントラクトに基づくアクセシブルなコンポーネントテストを書く開発者または割り当てられたエージェントです。
関連項目
vitest— ランナー、設定、カバレッジのしきい値、ウォッチ/CIを担当します。このスキルは、その上でコンポーネントテスト用の環境とRTL/MSW/axeによるテストの記述を担当します。tanstack-query— テスト対象のデータ取得フックです。このスキルでは、ライブラリ自体ではなく、テストごとのQueryClientProvider(retry:false)を紹介します。tanstack-router— ルーターテスト用ハーネス(createMemoryHistory+ テスト用RouterProvider)を担当します。ルーティング対象のコンポーネントでは、これをカスタムrenderに組み込みます。playwright-best-practices— e2eに対応するスキルです(実ブラウザー、@axe-core/playwright)。コンポーネントテストはその下位に位置します。あわせてreferences/browser-mode.mdも参照してください。biome— 静的なjsx-a11yのLint(Lint時)を担当します。vitest-axeは実行時の補完です。shadcn— テスト対象のコンポーネント/フォームです。
段階的な開示
references/msw-and-generated-clients.md— MSW v2のsetupServer/http/HttpResponse、Nodeのライフサイクル、テストごとのオーバーライド、リクエストボディの読み取り、クエリパラメーターに関する警告、生成された@hey-api/openapi-ts/openapi-fetchクライアントのインターセプト(リッスンしてから作成する順序 + カスタムfetchに関する注意点)を説明します。ネットワークをモックする場合に読み込んでください。references/accessibility-with-vitest-axe.md— vitest-axeのセットアップ方法、toHaveNoViolations、jsdomの要件、ルール設定、WCAGの対象範囲が限定的であることを説明します。a11yのアサーションを追加する場合に読み込んでください。references/states-and-forms.md— MSW + TanStack Queryを使った読み込み/空/成功/エラー状態のテストと、フォーム + リクエストボディ + セキュリティ不変条件のアサーションパターンを説明します。データ状態やフォームをテストする場合に読み込んでください。references/browser-mode.md— 公式に記載された代替手段である、Vitestブラウザーモード(Playwrightプロバイダー)+@axe-core/playwrightを説明し、トレードオフ(実行が遅い、CIの負荷が高い、a11yのテストがvitest-axeから移る、e2eスキルと重複する)を示します。デフォルトはjsdomです。実ブラウザーの忠実性が必要な場合にのみ読み込んでください。references/sources.md— 調査の出典情報と、ドキュメントでの検証対象リストです。
本文の上限
descriptionは1,024文字以下です。- 本文は約500行/5,000トークン以下です。大きな内容は
references/に置き、必要に応じて読み込みます。