Communityコーディング&開発github.com

Naki-404/naki-frontend-testing

Vitest と React Testing Library を使用したフロントエンドのユニットテストとコンポーネントテスト:振る舞い重視、アクセシブルなクエリ、user-event、MSW によるネットワークモッキング、フック、非同期 UI、カバレッジに対応。React コンポーネント、フック、またはページのテストを作成・修正する際や、テストが不安定・低速・実装の詳細に結合している場合に使用します。

naki-frontend-testing とは?

このスキルは、Vitest と React Testing Library を用いたフロントエンドテストのベストプラクティスを提供します。実際のユーザー体験に基づいた振る舞い重視のテスト記述を促進し、アクセシブルなクエリ(getByRole、getByLabelText など)の優先使用、userEvent を用いたユーザー操作のシミュレーション、MSW によるネットワークレイヤーでのモッキングを推奨します。実装の内部(状態変数、CSS クラス名、インスタンスメソッドなど)ではなく、出力(可視テキスト、ロール、フォーム値など)にアサーションを行うことで、テストの堅牢性とリファクタリング耐性を高めます。また、非同期 UI のテストには findBy* を推奨し、不安定なテストや実装結合の兆候(リファクタ後のテスト失敗)を検出するためのガイドラインも含まれています。カバレッジは目標ではなくシグナルとして扱い、エラー状態や空状態など重要なブランチの網羅を重視します。

対応~Claude Code~Codex CLI~Cursor
Part ofReact
npx skills add https://github.com/Naki-404/naki-claude-kit/tree/HEAD/kit/skills/naki-frontend-testing

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

フロントエンドテスト

ユーザーが見て操作するものをテストし、コンポーネントがどのように構築されているかはテストしません。ループのためには naki-tdd、完全なブラウザフローのためには naki-e2e-playwright、セマンティクスのためには naki-accessibility と組み合わせて使用します。プロジェクトにプロジェクトルールスキルがある場合(例:naki-myapp)は、それに従ってください。

使用するタイミング

  • 新しいコンポーネント、フック、またはページにテストが必要な場合。
  • 動作を変更しないリファクタリング後にテストが壊れた場合(実装結合の兆候)。
  • テストが不安定、waitFor をあちこちで使用、またはアプリの半分をモックしている場合。

コアルール

  1. ユーザーのようにクエリする。 優先順位:getByRole(name 付き)、getByLabelText、getByPlaceholderText、getByText、getByDisplayValue。アクセシブルな代替手段がない場合のみ getByTestId を使用します。
  2. fireEvent ではなく userEvent で操作する。 const user = userEvent.setup() で作成し、すべての呼び出しを await します。
  3. 出力にアサーションを行う: 可視テキスト、ロール、フォーム値、ネットワークエンドポイントへの呼び出し。状態変数、フックの内部、CSS クラス名、コンポーネントのインスタンスメソッドにはアサーションを行わないでください。
  4. モジュールではなくネットワークをモックする。 HTTP レイヤーで MSW ハンドラーを使用します。モジュールをモックするのは、真の外部境界(時間、乱数、jsdom にないブラウザ API)に対してのみにしてください。
  5. 後で表示されるものには findBy* を使用する。 waitFor はクエリではないアサーションのために予約し、waitFor 内に副作用を決して入れないでください。
  6. テストごとに1つの振る舞い。文のように名前を付けます:shows an error when the server returns 500。
  7. 各テストは自身の状態をセットアップする。 順序依存なし。afterEach でハンドラー、タイマー、ストレージをリセットします。
  8. 本物のプロバイダーを renderWithProviders ヘルパーで提供する(ルーター、リトライ無効のクエリクライアント、テーマ)。モックしないでください。
  9. アクセシビリティはテスト可能。 getByRole("button", { name: "Save" }) の失敗は、しばしば実際のアクセシビリティバグを示します。必要に応じて、主要な画面に vitest-axe チェックを追加しても構いません。
  10. カバレッジは目標ではなくシグナルです。 重要なブランチをカバーする:エラー状態、空状態、権限。

クエリ早見表

要素クエリ
ボタンgetByRole("button", { name: /save/i })
リンクgetByRole("link", { name: "Docs" })
ラベル付きテキスト入力getByRole("textbox", { name: "Email" }) または getByLabelText("Email")
チェックボックスgetByRole("checkbox", { name: "Remember me" })
見出しgetByRole("heading", { level: 2, name: "Alerts" })
テーブル行getAllByRole("row") 次に within(row)
ダイアログgetByRole("dialog", { name: "Delete alert" })
非存在expect(queryByText("Error")).not.toBeInTheDocument()

ワークフロー

  1. 振る舞いをリストアップ:レンダリング状態(ローディング、空、データ、エラー)とインタラクション。
  2. コンポーネントが呼び出すエンドポイントの MSW ハンドラーを追加。デフォルトは成功。
  3. 振る舞いごとに1つのテストを書き、エラーケースでは server.use(...) でハンドラーをオーバーライド。
  4. npx vitest run path/to/file.test.tsx を実行し、次にスイート全体を実行。
  5. 意図的に実装を一度壊す(一行コメントアウト)ことで、テストが失敗することを確認。

セットアップ

// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
import { afterAll, afterEach, beforeAll } from "vitest";
import { cleanup } from "@testing-library/react";
import { server } from "./src/test/server";

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => { server.resetHandlers(); cleanup(); });
afterAll(() => server.close());

onUnhandledRequest: "error" により、未処理のリクエストがあった場合にハングせずに失敗します。

例

import { http, HttpResponse } from "msw";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { server } from "../test/server";
import { AlertList } from "./AlertList";

test("lists alerts and filters by severity", async () => {
  server.use(http.get("/api/alerts", () => HttpResponse.json([
    { id: "a1", title: "Brute force on vpn", severity: "high" },
    { id: "a2", title: "New admin login", severity: "low" },
  ])));
  const user = userEvent.setup();
  render(<AlertList />);

  expect(await screen.findByText("Brute force on vpn")).toBeInTheDocument();
  await user.selectOptions(screen.getByRole("combobox", { name: "Severity" }), "high");
  expect(screen.queryByText("New admin login")).not.toBeInTheDocument();
});

test("shows a retry button when loading fails", async () => {
  server.use(http.get("/api/alerts", () => new HttpResponse(null, { status: 500 })));
  render(<AlertList />);
  expect(await screen.findByRole("button", { name: /retry/i })).toBeInTheDocument();
});

フックと時間

  • フックは、それを使用する小さなコンポーネントを通じてテストするか、UI がない場合は renderHook を使用してテストします。
  • 偽のタイマー:vi.useFakeTimers() と userEvent.setup({ advanceTimers: vi.advanceTimersByTime })。afterEach で vi.useRealTimers() で復元します。
  • 日付を固定するには vi.setSystemTime(new Date("2026-01-01T00:00:00Z")) を使用します。これによりスナップショットとラベルが安定します。

チェックリスト

  • クエリはロールまたはラベルベースである。
  • すべての userEvent 呼び出しが await されている。
  • ネットワークは MSW でモックされ、未処理のリクエストは失敗する。
  • ローディング、エラー、空の状態がカバーされている。
  • 内部やクラス名に対するアサーションがない。
  • テストは単独でも任意の順序でも成功する。
  • 出力に act(...) 警告がない。

よくある間違い

間違い修正
どこでも getByTestId を使用ロール/ラベルクエリを使用。コンポーネントにラベルがない場合は追加
await waitFor(() => getByText("x"))await findByText("x")
タイピングに fireEvent.change を使用await user.type(input, "text")
useAlerts フックを直接モックHTTP エンドポイントをモック
ページ全体の大きなスナップショット重要なものに明示的なアサーション
リトライ有効のクエリクライアント、遅いエラーテストテストプロバイダーで retry: false
act 警告を無視findBy* で非同期 UI 更新を待つ

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

関連スキル