react-testing-library について
React Testing Library を使ったユーザー視点のテスト作成を支援するリファレンススキルです。クエリ、操作、非同期処理、デバッグを扱います。
主な機能
- 01アクセシブルなクエリの優先順位
- 02一致数とタイミングに応じたクエリ選択
- 03ユーザーイベントの操作パターン
- 04非同期アサーションのパターン
- 05Hook と Provider のテスト
- 06DOM のデバッグと設定
活用シーン
- 01アクセシブルな UI テストのクエリ選択
- 02非同期コンテンツと操作のテスト
- 03Provider を使った Hook とコンポーネントのテスト
- 04レンダリングされた DOM のテスト失敗の調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ itechmeat/ llm-code --skill react-testing-libraryインストール後、Claude Code で/react-testing-libraryを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/api.mdasync.mdconfig.mddebugging.mdqueries.mduser-events.md詳細
React Testing Library スキル
クイックナビゲーション
| トピック | リンク |
|---|---|
| クエリ | references/queries.md |
| ユーザーイベント | references/user-events.md |
| API | references/api.md |
| 非同期 | references/async.md |
| デバッグ | references/debugging.md |
| 設定 | references/config.md |
インストール
インストール:npm install --save-dev @testing-library/react @testing-library/dom。推奨する追加パッケージ:@testing-library/user-event と @testing-library/jest-dom。React 19 には v16.1.0 以降が必要です。
基本理念
「テストがソフトウェアの実際の使われ方に近いほど、テストから得られる信頼性は高まります。」
テストを避ける対象:
- コンポーネントの内部状態
- 内部メソッド
- ライフサイクルメソッド
- 子コンポーネントの実装詳細
代わりにテストする対象:
- ユーザーに見えるものや、ユーザーが操作するもの
- ユーザーの視点から見た動作
- アクセシビリティ(ロールやラベルによるクエリ)
クエリの優先順位
以下の順にクエリを使用してください。
1. すべてのユーザーが利用できるクエリ(推奨)
// Best — by ARIA role
getByRole("button", { name: /submit/i });
getByRole("textbox", { name: /email/i });
// Form fields — by label
getByLabelText("Email");
// Non-interactive content — by text
getByText("Welcome back!");
2. セマンティッククエリ
// Images
getByAltText("Company logo");
// Title attribute (less reliable)
getByTitle("Close");
3. テスト ID(最後の手段)
// Only when other queries don't work
getByTestId("custom-element");
クエリの種類
| 種類 | 一致なし | 1 件一致 | 2 件以上一致 | 非同期 |
|---|---|---|---|---|
getBy... | エラーをスロー | 返す | エラーをスロー | いいえ |
queryBy... | null | 返す | エラーをスロー | いいえ |
findBy... | エラーをスロー | 返す | エラーをスロー | はい |
getAllBy... | エラーをスロー | 配列 | 配列 | いいえ |
queryAllBy... | [] | 配列 | 配列 | いいえ |
findAllBy... | エラーをスロー | 配列 | 配列 | はい |
使い分け:
getBy*— 要素が存在する場合queryBy*— 要素が存在しない可能性がある場合(expect(...).not.toBeInTheDocument()のようなアサーション)findBy*— 要素が非同期で表示される場合
基本的なテストパターン
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("shows greeting after login", async () => {
const user = userEvent.setup();
render(<App />);
// Act — simulate user interactions
await user.type(screen.getByLabelText(/username/i), "john");
await user.click(screen.getByRole("button", { name: /login/i }));
// Assert — verify outcome
expect(await screen.findByText(/welcome, john/i)).toBeInTheDocument();
});
ユーザーイベント
fireEvent よりも常に @testing-library/user-event を使用してください。
import userEvent from "@testing-library/user-event";
test("user interactions", async () => {
const user = userEvent.setup();
// Click
await user.click(element);
await user.dblClick(element);
await user.tripleClick(element);
// Type
await user.type(input, "Hello");
await user.clear(input);
// Select
await user.selectOptions(select, ["option1", "option2"]);
// Keyboard
await user.keyboard("{Enter}");
await user.keyboard("[ShiftLeft>]a[/ShiftLeft]"); // Shift+A
// Clipboard
await user.copy();
await user.paste();
// Pointer
await user.hover(element);
await user.unhover(element);
});
非同期パターン
waitFor — 成功するまで再試行
await waitFor(() => {
expect(screen.getByText("Loaded")).toBeInTheDocument();
});
// With options
await waitFor(() => expect(callback).toHaveBeenCalled(), {
timeout: 5000,
interval: 100,
});
findBy — waitFor が組み込まれています
// Equivalent to: await waitFor(() => getByText('Loaded'))
const element = await screen.findByText("Loaded");
waitForElementToBeRemoved
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
よくあるパターン
Provider を使用したカスタムレンダー
// test-utils.tsx
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";
import { AuthProvider } from "./AuthProvider";
function AllProviders({ children }) {
return (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
);
}
const customRender = (ui, options) => render(ui, { wrapper: AllProviders, ...options });
export * from "@testing-library/react";
export { customRender as render };
フックのテスト
import { renderHook, act } from "@testing-library/react";
test("useCounter increments", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
新しい props で再レンダー
const { rerender } = render(<Counter count={1} />);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
rerender(<Counter count={2} />);
expect(screen.getByText("Count: 2")).toBeInTheDocument();
コンテナー内のクエリ
import { within } from "@testing-library/react";
const modal = screen.getByRole("dialog");
const submitBtn = within(modal).getByRole("button", { name: /submit/i });
デバッグ
// Print entire DOM
screen.debug();
// Print specific element
screen.debug(screen.getByRole("button"));
// Log available roles
import { logRoles } from "@testing-library/react";
logRoles(container);
// With prettyDOM options
screen.debug(undefined, 10000); // max length
jest-dom マッチャー
import "@testing-library/jest-dom";
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEnabled();
expect(element).toBeDisabled();
expect(element).toHaveTextContent("Hello");
expect(element).toHaveValue("input value");
expect(element).toHaveAttribute("href", "/home");
expect(element).toHaveClass("active");
expect(element).toHaveFocus();
expect(element).toBeChecked();
設定
import { configure } from "@testing-library/react";
configure({
// Custom test ID attribute
testIdAttribute: "data-my-test-id",
// Async timeout
asyncUtilTimeout: 5000,
// Default hidden
defaultHidden: true,
// Throw suggestions (debugging)
throwSuggestions: true,
});
❌ 禁止事項(アンチパターン)
// ❌ Don't query by class/id
container.querySelector(".my-class");
// ❌ Don't use container.firstChild
const { container } = render(<Component />);
expect(container.firstChild).toHaveClass("active");
// ❌ Don't use fireEvent when userEvent works
fireEvent.click(button); // Use userEvent.click instead
// ❌ Don't test implementation details
expect(component.state.loading).toBe(false);
// ❌ Don't use waitFor with findBy
await waitFor(() => screen.findByText("x")); // findBy already waits
// ❌ Don't assert inside waitFor callback (unless necessary)
await waitFor(() => {
expect(mockFn).toHaveBeenCalled(); // OK - need to wait for call
});
✅ ベストプラクティス
// ✅ Use screen for all queries
import { render, screen } from "@testing-library/react";
render(<Component />);
screen.getByRole("button"); // Good
// ✅ Prefer userEvent over fireEvent
const user = userEvent.setup();
await user.click(button);
// ✅ Use findBy for async elements
const element = await screen.findByText("Loaded");
// ✅ Use queryBy for non-existence assertions
expect(screen.queryByText("Error")).not.toBeInTheDocument();
// ✅ Use within for scoped queries
const form = screen.getByRole("form");
within(form).getByLabelText("Email");
// ✅ Use accessible queries (role, label, text)
getByRole("button", { name: /submit/i });
TextMatch のオプション
// Exact match (default)
getByText("Hello World");
// Substring match
getByText("llo Worl", { exact: false });
// Regex
getByText(/hello world/i);
// Custom function
getByText((content, element) => {
return element.tagName === "SPAN" && content.startsWith("Hello");
});
クイックリファレンス
| インポート | 用途 |
|---|---|
render | コンポーネントを DOM にレンダーする |
screen | レンダーされた DOM をクエリする |
cleanup | コンポーネントをアンマウントする(Jest では自動) |
act | 状態の更新をラップする |
renderHook | カスタムフックをテストする |
within | クエリの対象を要素内に限定する |
waitFor | アサーションが成功するまで再試行する |
configure | グローバルオプションを設定する |
userEvent.setup() | ユーザーイベントのインスタンスを作成する |
リンク
コメント
Markdown 対応