mcpField
R
react-testing-library

react-testing-library

github.com/itechmeatテスト・品質241.4k 回インストール4 日前に更新
リポジトリ

react-testing-library について

React Testing Library を使ったユーザー視点のテスト作成を支援するリファレンススキルです。クエリ、操作、非同期処理、デバッグを扱います。

主な機能

  1. 01アクセシブルなクエリの優先順位
  2. 02一致数とタイミングに応じたクエリ選択
  3. 03ユーザーイベントの操作パターン
  4. 04非同期アサーションのパターン
  5. 05Hook と Provider のテスト
  6. 06DOM のデバッグと設定

活用シーン

  1. 01アクセシブルな UI テストのクエリ選択
  2. 02非同期コンテンツと操作のテスト
  3. 03Provider を使った Hook とコンポーネントのテスト
  4. 04レンダリングされた DOM のテスト失敗の調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/itechmeat/llm-code --skill react-testing-library

インストール後、Claude Code で/react-testing-libraryを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
api.md
async.md
config.md
debugging.md
queries.md
user-events.md

詳細

React Testing Library スキル

クイックナビゲーション

トピックリンク
クエリreferences/queries.md
ユーザーイベントreferences/user-events.md
APIreferences/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()ユーザーイベントのインスタンスを作成する

リンク

コメント