frontend-testing について
Web アプリのテストを作成、保守する開発者向けのフロントエンドテストガイドです。受け入れ基準とユーザーに見える動作から始め、ユニット、E2E、ビジュアル、アクセシビリティの各テストを選ぶ方法を示します。
主な機能
- 01受け入れ基準からテストを計画
- 02受け入れテストと実装テストを区別
- 03Vitest、Testing Library、Playwright を紹介
- 04セマンティックなロケーターを優先
- 05ARIA スナップショットとアクセシビリティテスト
- 06アンチパターンと失敗時の調査方法
活用シーン
- 01ユーザーストーリーに基づく受け入れテスト
- 02コンポーネントの動作や純粋なロジックのテスト
- 03ブラウザーでのユーザーフローの検証
- 04UI 構造とアクセシビリティの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ schalkneethling/ webdev-agent-skills --skill frontend-testingインストール後、Claude Code で/frontend-testingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/accessibility-testing.mdaria-snapshots.mdlocator-strategies.mdvisual-regression.md詳細
フロントエンドテスト
まず、受け入れ基準を検証するテストを作成します。その後、価値がある場合は実装テストを追加します。
基本原則
「テストがソフトウェアの使われ方に近いほど、テストから得られる信頼性は高まります。」 — Kent C. Dodds
この原則はテストに関する判断の指針となりますが、すべてではありません。
- 受け入れ基準テストは、システムがユーザーやステークホルダーのニーズを満たしていることを検証します。これらのテストは、リファクタリング後も安定しているべきです。
- 実装テストは、各要素の堅牢性を検証します。エッジケース、エラー処理、複雑なロジックなどが対象です。リファクタリング時に変更が必要になる場合があります。
どちらにも価値があります。避けるべきアンチパターンは、意味のある振る舞いを検証せず、実装だけをなぞるテストです。
参照資料を読み込むタイミング
テストの種類に応じて、参照ファイルを読み込みます。
- DOM を使ったユニットテスト:
references/locator-strategies.md - E2E テスト:
references/locator-strategies.md、references/aria-snapshots.md - ビジュアルリグレッションテスト:
references/visual-regression.md - アクセシビリティ監査:
references/accessibility-testing.md - 構造の検証:
references/aria-snapshots.md— 複数のアサーションを1つにまとめます - すべてのテスト:基本的なワークフローを確認するため、まずこのファイルを読みます
ワークフロー
ステップ 1:受け入れ基準から始める
テストを書く前に、ユーザーの視点からコードがどのように動作すべきかを特定します。
以下から基準を確認、または抽出します。
- チケットの説明またはユーザーストーリー
- Figma の注釈
- 機能要件
- プロダクトオーナーへの確認
基準をチェックリストとして記録します。これが最初のテストになります。
**実装を読む前に受け入れテストを書きます。**これにより、テストが単に「コードがコードどおりに動作する」ことを確認するだけの循環的な検証を防げます。
ステップ 2:基準をテストケースに対応付ける
各基準について、以下を特定します。
- 正常系:通常想定される動作
- エッジケース:境界条件
- エラーケース:無効な入力、失敗
対応付けの例:
Criterion: "User can filter products by category"
├─ Happy path: Select category, products filter correctly
├─ Edge case: No products match filter, show empty state
├─ Edge case: Clear filter, all products show again
├─ Error case: Filter API fails, show error message
└─ Accessibility: Filter controls are keyboard accessible
ステップ 3:実装テストを追加する(ユニットテスト)
受け入れテストがパスしたら、実装の堅牢性を確認するユニットテストを追加します。
- 基準でカバーされていないエッジケース(null、undefined、空の配列、境界値)
- 複雑な計算のアルゴリズムの正確性
- エラー処理の経路(例外、ネットワーク障害、解析エラー)
- 統合テストでは検証しにくい複雑な分岐ロジック
- 特別な検証が必要なパフォーマンス重視のコード
Function: filterProducts(products, category)
├─ Acceptance: Returns matching products (from criteria)
├─ Implementation: Returns empty array when products is null
├─ Implementation: Returns all products when category is empty string
├─ Implementation: Handles case-insensitive category matching
└─ Implementation: Does not mutate original array
違いは、受け入れテストはリファクタリング時にほとんど変更されない一方、実装テストは内部の変更に合わせて更新が必要になる場合があることです。
ステップ 4:テストの種類を選ぶ
| シナリオ | テストの種類 | ツール |
|---|---|---|
| 純粋なロジック(DOM なし) | ユニットテスト | Vitest |
| コンポーネントの振る舞い | DOM を使ったユニットテスト | Vitest + Testing Library |
| ユーザーフロー、実ブラウザー | E2E テスト | Playwright |
| セマンティックな構造の検証 | ARIA スナップショット | Playwright toMatchAriaSnapshot |
| 視覚的な外観 | VRT | Playwright のスクリーンショット |
| アクセシビリティへの準拠 | A11y テスト | Playwright + axe-core |
ARIA スナップショットは、E2E テストで特に有用です。単一のスナップショットで複数の個別アサーションを置き換えながら、アクセシビリティツリーの構造を検証できます。
**ユニットテストの DOM 環境:**jsdom より happy-dom を優先してください。より高速であり、API の制限が有用な判断材料になります。テスト対象が happy-dom でサポートされていない場合は、代わりに E2E テストで扱うべきか検討してください。
ステップ 5:コードを書く前、またはコードと並行してテストを書く
- 理想的:先にテストを書き、その後で実装する(TDD)
- 許容される:各基準の実装直後にテストを書く
- 避けるべき:実装が「完了」してから、すべてのテストを書く
テストの構成
ユニットテスト(Vitest)
describe("calculateDiscount", () => {
describe("when customer has premium membership", () => {
it("applies 20% discount to order total", () => {
// Arrange - Set up test data matching criterion
const order = { total: 100, membership: "premium" };
// Act - Call the function
const result = calculateDiscount(order);
// Assert - Verify expected outcome from requirements
expect(result).toBe(80);
});
});
});
コンポーネントテスト(Vitest + Testing Library)
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
describe("LoginForm", () => {
describe("when credentials are invalid", () => {
it("displays error message to user", async () => {
const user = userEvent.setup();
render(<LoginForm />);
// Interact using accessible queries
await user.type(
screen.getByLabelText(/email/i),
"[email protected]"
);
await user.type(
screen.getByLabelText(/password/i),
"wrong"
);
await user.click(
screen.getByRole("button", { name: /sign in/i })
);
// Assert on user-visible outcome
expect(
await screen.findByRole("alert")
).toHaveTextContent(/invalid credentials/i);
});
});
});
E2E テスト(Playwright)
import { test, expect } from "@playwright/test";
test.describe("Product Catalog", () => {
test.describe("filtering by category", () => {
test("shows only matching products", async ({ page }) => {
await page.goto("/products");
// Use semantic locators
await page.getByRole("combobox", { name: /category/i }).selectOption("Electronics");
// Assert count, then spot-check first/last
const products = page.getByRole("article");
await expect(products).toHaveCount(5);
await expect(products.first()).toContainText(/electronics/i);
await expect(products.last()).toContainText(/electronics/i);
});
});
});
すべての項目を検証する必要がある場合は、アサーションを並列実行するために Promise.all を使用します。
test("all products match filter", async ({ page }) => {
await page.goto("/products");
await page.getByRole("combobox", { name: /category/i }).selectOption("Electronics");
const products = await page.getByRole("article").all();
// Parallel assertions — faster than sequential await in a loop
await Promise.all(
products.map(product =>
expect(product.getByText(/electronics/i)).toBeVisible()
)
);
});
ARIA スナップショットを使った E2E テスト
ARIA スナップショットを使うと、複数のアサーションをまとめて、セマンティックな構造を検証できます。
test.describe("Login Page", () => {
test("has correct form structure", async ({ page }) => {
await page.goto("/login");
// One snapshot replaces 5+ individual assertions
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- heading "Sign In" [level=1]
- textbox "Email"
- textbox "Password"
- button "Sign In"
- link "Forgot password?"
`);
});
test("shows validation errors on empty submit", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: /sign in/i }).click();
await expect(page.getByRole("form")).toMatchAriaSnapshot(`
- textbox "Email"
- text "Email is required"
- textbox "Password"
- text "Password is required"
- button "Sign In"
`);
});
});
ロケーターの優先順位
ユーザーや支援技術が要素を見つける方法を反映したロケーターを使用してください。
getByRole— 第一候補です。アクセシビリティツリーを検索します。getByLabelText— フォームフィールドに最適です。ユーザーはラベルから入力欄を見つけます。getByPlaceholderText— ラベルがない場合に使います(理想的ではありません)。getByText— 操作できない要素に使います。getByAltText— 画像に使います。getByTestId— 最後の手段として使います。
セマンティックなクエリで要素を見つけられない場合、UI にアクセシビリティ上の問題がある可能性があります。
アンチパターン
実装の詳細だけをテストする
意味のある動作を検証せず、内部だけを確認するテストです。
// BAD: Tests internal method exists, provides no behavior guarantee
it("has a validateFields method", () => {
expect(form.#validateFields).toBeDefined();
});
// BAD: Asserts implementation without verifying outcome
it("calls the validator", () => {
expect(mockValidator).toHaveBeenCalledWith(data);
});
// GOOD: Tests observable behavior (acceptance)
it("prevents submission with invalid email", async () => {
await user.type(emailInput, "not-an-email");
await user.click(submitButton);
expect(screen.getByRole("alert")).toHaveTextContent(/valid email/i);
});
// ALSO GOOD: Tests implementation robustness (unit)
it("returns validation errors for malformed email", () => {
const result = validateEmail("not-an-email");
expect(result.valid).toBe(false);
expect(result.error).toBe("Invalid email format");
});
重要な違いは、実装テストでは、コードパスが実行されることだけでなく、ユニットの意味のある動作を検証する必要があるという点です。
循環的な検証
// BAD: Test data derived from implementation
const expected = formatPrice(100); // Don't compute expected from code!
expect(formatPrice(100)).toBe(expected);
// GOOD: Expected value from requirements
expect(formatPrice(100)).toBe("$100.00");
過剰なモック
// BAD: Mock everything, test nothing real
jest.mock("./api");
jest.mock("./utils");
jest.mock("./formatter");
// Now just testing mocks talk to each other
// GOOD: Mock only external boundaries
// Mock: APIs, databases, time, file system
// Real: Business logic, components, formatters
脆弱なセレクター
// BAD: Implementation-dependent selectors
page.locator(".btn-primary.submit-form");
page.locator("#root > div > form > button:nth-child(3)");
// GOOD: Semantic locators
page.getByRole("button", { name: /submit order/i });
テストの失敗は通常、バグを意味します
テストが失敗した場合、調査方法はテストの種類によって異なります。
受け入れテストが失敗した場合:
- まずテスト対象のコードを確認します。実際のバグである可能性が高いです。
- テストが現在の要件に合っているか確認します。要件が変更されている可能性があります。
- 新しい動作が正しいと確認してから、テストを更新します。
実装テストが失敗した場合:
- リファクタリング中であれば、正当にテストの更新が必要な場合があります。
- リファクタリング中でなければ、コードを確認します。バグである可能性が高いです。
- テストが実装に強く結び付きすぎていないか検討します。
テストに合格させるために、反射的にテストを更新しないでください。失敗の原因を調査してください。
テストを提出する前のチェックリスト
- 各受け入れ基準に少なくとも1つのテストがある
- 基準に含まれるエッジケースをカバーしている
- 複雑なロジック、エラー処理、境界条件に対する実装テストを追加している
- テストに説明的な名前を付けている(基準ベースまたは動作ベース)
- セマンティックなロケーター(getByRole、getByLabelText)を使用している
- 意味のある動作を検証せず、「コードが実行されること」だけを確認するテストがない
- 失敗したテストを適切に調査している(受け入れテストか実装テストかを考慮)
- インタラクティブなコンポーネントにアクセシビリティチェックを含めている
- 構造の検証には、複数のアサーションをまとめられる ARIA スナップショットを検討している
参考資料
詳しいガイダンスについては、以下のファイルを参照してください。
references/locator-strategies.md— ロケーターの完全な優先順位と例references/aria-snapshots.md— 構造の検証とアサーションの集約references/accessibility-testing.md— axe-core の統合、WCAG の対象設定references/visual-regression.md— スクリーンショットテスト、ベースライン管理