# zudo-test-wisdom > Takazudo's frontend testing strategy guide for AI agents and developers --- # 判断ガイド > Source: /pj/zudo-test/ja/docs/decision-guide ## クイック判断テーブル このテーブルを使って、現在のタスクに必要な最小テストレベルを判断します: | 変更内容 | 最小レベル | 理由 | |---------|----------|------| | 純粋なロジック/ユーティリティ関数 | レベル1 | DOMやCSSが関与しない | | コンポーネントのprops/状態 | レベル2 | 出力を検証するためにシミュレートDOMが必要 | | ビルド設定/テンプレート/SSG | レベル3 | ビルドされた出力ファイルを検査する必要がある | | CSS/レイアウト/表示 | レベル5 | CSSは実際のレンダリングエンジンが必要 | | インタラクティブなUIフロー | レベル4 | ユーザーインタラクションには実際のブラウザが必要 | | ビジュアルバグの報告 | レベル5 | 算出スタイル + 視覚的結果を確認する必要がある | | 「表示されない」 | レベル5 | 表示は視覚的な属性 | | 「まだ壊れている」(テストがパスした後) | 1つ上のレベル | 現在のレベルはこのバグに対するブラインドスポットがある | **「最小レベル」とは、バグを確実にキャッチできる最低のレベルを意味します。** より低いレベルを使用すると誤った確信を与えます -- テストはパスしますが、バグは残ります。 ## 判断フローチャート ```mermaid flowchart TD A[何を検証していますか?] --> B{純粋なロジックですか?} B -->|はい| L1[レベル1: ユニットテスト] B -->|いいえ| C{コンポーネントの動作ですか?} C -->|はい| D{CSS/表示に関わりますか?} D -->|いいえ| L2[レベル2: DOMコンポーネントテスト] D -->|はい| L5a[レベル5: 視覚的検証] C -->|いいえ| E{ビルド出力ですか?} E -->|はい| L3[レベル3: ビルド出力テスト] E -->|いいえ| F{インタラクティブUIですか?} F -->|はい| L4[レベル4: E2Eブラウザテスト] F -->|いいえ| G{ビジュアル/CSSですか?} G -->|はい| L5b[レベル5: 視覚的検証] G -->|いいえ| L1b[レベル1: ユニットテストから開始] ``` ## 重要な原則: CSSは常にレベル5が必要 CSS、レイアウト、視覚的な外観に関わる変更はレベル5をデフォルトにすべきです。その理由: 1. **レベル1**(ユニットテスト)-- DOMがまったくなく、CSSを処理できない 2. **レベル2**(jsdom)-- DOMはあるがCSSエンジンがない;`getComputedStyle()`は空文字列を返す 3. **レベル3**(ビルド出力)-- ファイルの内容を検査するが、レンダリングは検査しない 4. **レベル4**(Playwright)-- 実際のブラウザで実行するが、通常は視覚的な外観ではなくDOMの状態をアサートする レベル5(verify-ui + headless-browser)のみが、算出スタイルの値を決定論的にチェックし、結果を視覚的に確認できます。 ## エスカレーションのトリガー 以下の場合に次のレベルに進みます: - テストがパスしたがユーザーが問題の継続を報告した - ロジックをテストしているがバグが視覚的な可能性がある - 下位レベルのテストでデータの正しさが確認されたが出力が正しく見えない - CSSまたはレイアウトの問題が疑われる - 複数の下位レベルのテストがパスしたが機能がブラウザで動作しない --- # 概要 > Source: /pj/zudo-test/ja/docs/overview [Takazudo](https://x.com/Takazudo)の個人的な開発ノートです。公式のテストドキュメントではありません。 個人的な参照とAI支援コーディングのために書かれています。 このサイトはAI支援開発のための**フロントエンドテスト戦略**を扱っています。AIコーディングエージェントと一緒に作業する際に適切なテストアプローチを選択すること -- ユニットテストで十分な場合、ブラウザレベルの検証が必要な場合、そしてパスしたテストが実際に重要なことを検証していないという誤った確信の一般的な罠を避ける方法に焦点を当てています。 ## このサイトで扱う内容 ここに記載されている主要なトピックは、すべて実際の本番プロジェクトから抽出されたパターンに基づいています: - **テストレベル** -- ユニットテストから視覚的検証までの5段階エスカレーションラダー。各レベルはカバレッジが増加し、ブラインドスポットが減少します - **判断ガイド** -- 変更内容に基づいてどのテストレベルを使用するか、間違ったレベルが選択された場合の一般的な失敗パターン、AIエージェントに必要な振る舞い - **実践パターン** -- 実戦で検証済みのVitest設定、Playwright E2Eパターン、Tauriデスクトップアプリテスト、バックエンド/Node.jsテストアプローチ - **ツールリファレンス** -- 各テストレベルのツールとコマンドのクイックルックアップ このガイドは主に**フロントエンドに焦点**を当てています。著者の強みがフロントエンド開発にあるためです。バックエンドテストは補足的なトピックとして扱っています -- 具体的には、フロントエンドとバックエンドが適切に分離され、それぞれ独立してテストできる場合に現れるパターンです。 ## 核心的なインサイト AI支援テストにおいて最も重要な概念は**テストレベルのエスカレーション**です。AIエージェントがバグを修正して解決済みと宣言する際、その修正の信頼性は検証に使用したテスト手法に依存します。ユニットテストはロジックの正しさを確認できますが、ユーザーが実際に画面上で結果を見られるかどうかは確認できません。「ロジック的に正しい」と「視覚的に正しい」の間のギャップが、誤った確信の最も一般的な原因です。 テストは繰り返すのではなく、エスカレーションすべきです。下位レベルのテストがパスしても問題が解消しない場合は、次のレベルに進みます: 1. **レベル1** -- ユニット/ロジックテスト(vitest、jest) 2. **レベル2** -- DOMベースのコンポーネントテスト(jsdom、Testing Library) 3. **レベル3** -- ビルド出力検証(ビルドされたファイルを読む) 4. **レベル4** -- E2Eブラウザテスト(Playwright、headless browser) 5. **レベル5** -- 決定論的 + 視覚的検証(verify-ui + headless browser) ## このガイドの使い方 1. まず[判断ガイド](../decision-guide/index.mdx)で、現在のタスクに適したテストレベルを判断します 2. [テストレベル](../testing-levels/index.mdx)セクションで各レベルの詳細を確認します 3. [実践パターン](../real-world-patterns/index.mdx)で本番環境で検証済みの設定を参照します 4. [ツールリファレンス](../tools-reference/index.mdx)をクイックルックアップ用に開いておきます --- # 実践パターン > Source: /pj/zudo-test/ja/docs/real-world-patterns ## 本番環境で検証済みのアプローチ このセクションのパターンは、理論的なベストプラクティスではなく、実際の本番プロジェクトから得られたものです。各パターンは出荷されたソフトウェアで使用され、実際のバグや障害を通じて洗練されています。 ## ソースプロジェクト | プロジェクト | タイプ | 主要なテストパターン | |------------|-------|-------------------| | **zudo-text** | Tauriテキストエディタ | モックバックエンドアダプター、コンソールエラーモニタリング、@interactiveキーボードテスト | | **zmod** | Webアプリケーション | 本番ビルドPlaywright、CI画像インターセプト、シャードE2E | | **zudo-pattern-gen** | パターンジェネレーター | 決定論的PNGレンダリング、Miniflare + D1/R2インテグレーション | | **mdx-formatter** | CLIツール | VitestによるRustのコントラクトテスト、冪等性不変条件 | ## パターンカテゴリ ### [Vitestパターン](/pj/zudo-test-wisdom/ja/docs/real-world-patterns/vitest-patterns) ワークスペース設定、jsdom/happy-dom環境、コントラクトテスト、冪等性テスト、Miniflareインテグレーションテスト。 ### [Playwrightパターン](/pj/zudo-test-wisdom/ja/docs/real-world-patterns/playwright-patterns) CI安全なテスト分割、コンソールエラーモニタリング、画像インターセプト、本番ビルド検証、シャードCIラン。 ### [Tauriテスト](/pj/zudo-test-wisdom/ja/docs/real-world-patterns/tauri-testing) WebKit専用ルール、コアクレートパターン、バックエンドブリッジモック、デスクトップアプリ向けの完全な8ステップエスカレーションラダー。 ### [バックエンド & Node.js テスト](/pj/zudo-test-wisdom/ja/docs/real-world-patterns/backend-testing) Cloudflare FunctionsとMiniflare、HTTP APIテスト、`vi.stubGlobal`によるfetchモック、一時ディレクトリを使ったファイルシステムテスト、フロントエンドとバックエンドのテスト設定を分離するための重要な原則。 ## 共通テーマ すべてのプロジェクトを通じて、1つのテーマが浮かび上がります:**テストアプローチはデプロイ先に合わせなければなりません**。Webアプリにはブラウザレベルのテストが必要です。CLIツールには出力検証が必要です。Tauriアプリには WebKit固有のテストが必要です。普遍的なテスト設定は存在せず、特定のコンテキストに適したテスト設定があるだけです。 --- # 5つのテストレベル > Source: /pj/zudo-test/ja/docs/testing-levels ## 概要 フロントエンドテストは単一の活動ではなく、それぞれ異なる能力とブラインドスポットを持つ検証手法のスペクトラムです。このセクションでは、検証可能な範囲の順に5つのレベルを定義します。 ```mermaid graph LR L1[Level 1Unit/Logic] --> L2[Level 2DOM Component] L2 --> L3[Level 3Build Output] L3 --> L4[Level 4E2E Browser] L4 --> L5[Level 5Visual Verify] ``` ## サマリーテーブル | レベル | 名称 | ツール | 検証可能 | ブラインドスポット | |-------|------|-------|---------|-----------------| | 1 | ユニット/ロジック | vitest、jest | 純粋関数、データ変換、状態ロジック | DOM、CSS、レンダリング | | 2 | DOMコンポーネント | vitest + jsdom、Testing Library | コンポーネント出力、props、DOM構造 | 視覚的レンダリング、CSS | | 3 | ビルド出力 | vitest(ビルドファイル読み取り) | SSG出力、テンプレート、バンドラ設定 | ランタイム動作、視覚的表示 | | 4 | E2Eブラウザ | Playwright、headless-browser | ユーザーインタラクション、ナビゲーション、ページ全体 | 微妙な視覚的詳細 | | 5 | 決定論的 + 視覚的 | verify-ui + headless-browser | 算出スタイル、ピクセルレベルレンダリング | 最小限のブラインドスポット | ## エスカレーションルール 現在のレベルのテストがパスしたにもかかわらず、ユーザーが問題が解消していないと報告した場合、同じテストを再実行しないでください。次のレベルにエスカレーションしてください。 レベルはカバレッジの広さ順に並んでいます。各上位レベルは、下位レベルでは構造的に検出不可能なバグのカテゴリをキャッチします。たとえば、ユニットテストはCSSをまったく処理しないため、`overflow: hidden`で要素が隠されていることを検出できません。 ## 適切なレベルの選択 すべてのタスクにレベル5が必要なわけではありません。目標は、テストレベルを変更の性質に合わせることです: - **ロジックの変更** -- レベル1で十分 - **コンポーネントの動作** -- レベル2でカバー - **ビルド設定** -- レベル3が対象 - **インタラクティブなフロー** -- レベル4が必要 - **視覚的/CSSのバグ** -- レベル5が必須 詳細なマッピングテーブルは[判断ガイド](/pj/zudo-test-wisdom/ja/docs/decision-guide)を参照してください。 --- # ツールリファレンス > Source: /pj/zudo-test/ja/docs/tools-reference ## テストレベル別ツール | レベル | ツール | インストール | 実行コマンド | |-------|-------|------------|------------| | 1 | vitest | `pnpm add -D vitest` | `pnpm vitest` | | 1 | jest | `pnpm add -D jest` | `pnpm jest` | | 2 | vitest + jsdom | `pnpm add -D vitest jsdom` | `pnpm vitest` | | 2 | vitest + happy-dom | `pnpm add -D vitest happy-dom` | `pnpm vitest` | | 2 | @testing-library/react | `pnpm add -D @testing-library/react` | (テスト内で使用) | | 3 | vitest(ビルドファイル読み取り) | `pnpm add -D vitest` | `pnpm build && pnpm vitest --project build` | | 4 | Playwright | `pnpm add -D @playwright/test` | `npx playwright test` | | 4 | headless-browser | (Claude Codeスキル) | `/headless-browser` | | 5 | verify-ui | (Claude Codeスキル) | `/verify-ui` | | 5 | headless-browser | (Claude Codeスキル) | `/headless-browser` | ## ツール機能マトリクス | 機能 | vitest | vitest+jsdom | Playwright | verify-ui | headless-browser | |------|--------|-------------|------------|-----------|-----------------| | 純粋関数テスト | はい | はい | -- | -- | -- | | DOM構造 | -- | はい | はい | -- | -- | | ユーザーイベント | -- | はい | はい | -- | -- | | CSS算出スタイル | -- | -- | はい | **はい** | -- | | 視覚的スクリーンショット | -- | -- | はい | -- | **はい** | | コンソールエラー | -- | -- | はい | -- | はい | | 複数ページナビゲーション | -- | -- | はい | -- | はい | | ビルド出力 | はい | -- | -- | -- | -- | | レスポンシブビューポート | -- | -- | はい | はい | はい | ## Vitest設定クイックリファレンス ### 最小限のユニットテストセットアップ ```typescript // vitest.config.ts test: { include: ["src/**/*.test.ts"], }, }); ``` ### コンポーネントテストセットアップ(jsdom) ```typescript // vitest.config.ts test: { environment: "jsdom", include: ["src/**/*.test.tsx"], setupFiles: ["./test-setup.ts"], }, }); ``` ### ワークスペースセットアップ(複数テストタイプ) ```typescript // vitest.workspace.ts { test: { name: "unit", include: ["src/**/*.test.ts"], environment: "node" }, }, { test: { name: "component", include: ["src/**/*.test.tsx"], environment: "jsdom", }, }, { test: { name: "build", include: ["tests/build/**/*.test.ts"], environment: "node", }, }, ]); ``` ## Playwright設定クイックリファレンス ### 基本セットアップ ```typescript // playwright.config.ts testDir: "./e2e", use: { baseURL: "http://localhost:3000", }, webServer: { command: "pnpm dev", port: 3000, reuseExistingServer: !process.env.CI, }, }); ``` ### WebKit専用(Tauri向け) ```typescript // playwright.config.ts projects: [ { name: "webkit", use: { ...devices["Desktop Safari"] } }, ], }); ``` ### 本番ビルドテスト ```typescript // playwright.config.ts webServer: { command: "pnpm build && pnpm preview", port: 4173, reuseExistingServer: !process.env.CI, }, }); ``` ## Claude Codeスキルコマンド | コマンド | レベル | 目的 | |---------|-------|------| | `/headless-browser` | 4、5 | スクリーンショット撮影、コンソールエラーチェック、ページ操作 | | `/verify-ui` | 5 | 算出CSS値を決定論的にアサート | | `/test-wisdom` | -- | どのテストレベルを使用すべきかのガイダンスを取得 | --- # AIの典型的な失敗パターン > Source: /pj/zudo-test/ja/docs/decision-guide/common-failure-pattern ## パターン AI支援開発における最も頻繁なテスト失敗は、予測可能なパターンに従います: 1. ユーザーが報告:**「表示されない」** または **「まだ壊れている」** 2. AIエージェントがユニットテストを書くかロジックをチェックする 3. ロジックテストがパス -- データは正しく、コンポーネントは正しいJSXを返す 4. AIエージェントが宣言:**「修正完了!テストがパスしました。」** 5. ユーザーが報告:**「まだ表示されない。」** 6. AIエージェントが同じテストを再実行し、同じパス結果を得る 7. ユーザーの信頼を失うまでサイクルが繰り返される ## なぜ起こるか AIエージェントが**レベル5(視覚的検証)**を必要とする問題に対して**レベル1(ユニットテスト)**または**レベル2(DOMテスト)**を選択しています。 データとロジックは正しいです。コンポーネントはDOMツリーに正しい要素をレンダリングします。しかし、祖先チェーンのどこかにあるCSSルールが要素を非表示にしています。 ## 具体例 開発者がAIに通知バナーの追加を依頼します。AIがコンポーネントを作成します: ```tsx // NotificationBanner.tsx return ( {message} ); } ``` そしてテストを書きます: ```tsx // NotificationBanner.test.tsx it("renders the message", () => { render(); expect(screen.getByText("Update available")).toBeTruthy(); }); ``` テストはパスします。しかしユーザーは画面に何も見えません。なぜでしょうか? ```css /* layout.css -- inherited from the page layout */ .main-content { overflow: hidden; max-height: 0; transition: max-height 0.3s ease; } .main-content.expanded { max-height: 1000px; } ``` 通知バナーは `.main-content` 内にレンダリングされており、デフォルトで `max-height: 0` と `overflow: hidden` が設定されています。要素はDOMに存在します(レベル2パス)が、高さ0にクリップされて視覚的に非表示です(レベル5ならキャッチ)。 ## 修正方法 ユーザーが何かが「表示されない」と言った場合、デフォルトでレベル5の検証を使用してください。要素とその祖先の**算出スタイル**をチェックし、**スクリーンショット**を撮影して視覚的な状態を確認します。 レベル5の検証で以下が明らかになります: ``` verify-ui result: .main-content { overflow: hidden // <-- clipping children max-height: 0px // <-- zero height } .notification-banner { display: block // present in DOM // but parent clips it to invisible } ``` headless-browserのスクリーンショットにより、バナーが表示されていないことが視覚的に確認されます。 ## このパターンの他のバリエーション `overflow: hidden` + `height: 0` パターンはバリエーションの1つに過ぎません。他のよくある原因: | CSSプロパティ | 効果 | レベル2で検出? | レベル5で検出? | |-------------|------|---------------|---------------| | `display: none` | 要素がフローから除去される | いいえ | はい | | `visibility: hidden` | 要素は非表示だがスペースを占める | いいえ | はい | | `opacity: 0` | 要素が完全に透明 | いいえ | はい | | `z-index`のスタッキング | 要素が別の要素の背後に | いいえ | はい | | 祖先の`overflow: hidden` | コンテンツがクリップされる | いいえ | はい | | `transform: scale(0)` | 要素が何もないサイズに縮小 | いいえ | はい | | `position: absolute` + 画面外 | 要素がビューポート外に配置 | いいえ | はい | ## 重要なポイント **ロジックテストに基づいてビジュアルバグを修正済みと宣言しないでください。** ユーザーが何かが見えないと言う場合、テストは表示を検証する必要があり、それにはレベル5が必要です。 --- # Vitestパターン > Source: /pj/zudo-test/ja/docs/real-world-patterns/vitest-patterns ## ワークスペースレベルのVitest設定 大規模なプロジェクトでは、異なるテストタイプに異なるVitest設定が必要になることがよくあります。Vitestワークスペースで管理します: ```typescript // vitest.workspace.ts { test: { name: "unit", include: ["src/**/*.test.ts"], environment: "node", }, }, { test: { name: "component", include: ["src/**/*.test.tsx"], environment: "jsdom", }, }, { test: { name: "build", include: ["tests/build/**/*.test.ts"], environment: "node", }, }, ]); ``` 設定を分離することで、高速なユニットテストを低速なコンポーネントテストやビルドテストとは独立して実行できます:`vitest --project unit` vs `vitest --project component`。 ## jsdomとhappy-dom環境 コンポーネントテストに適切なDOM環境を選択します: ```typescript // vitest.config.ts for component tests test: { environment: "jsdom", // Or use happy-dom for faster execution: // environment: "happy-dom", globals: true, setupFiles: ["./test-setup.ts"], }, }); ``` 必要に応じてファイル単位の環境オーバーライドも可能です: ```typescript // @vitest-environment jsdom describe("DOM-dependent test", () => { it("manipulates the document", () => { document.body.innerHTML = 'Hello'; expect(document.getElementById("app")?.textContent).toBe("Hello"); }); }); ``` ## テストタイプ別の設定分離 mdx-formatterのパターン:ユニットテスト、APIテスト、関数テスト用の別々の設定: ``` vitest.config.ts # Default: unit tests vitest.config.api.ts # API integration tests vitest.config.functions.ts # Cloud function tests ``` ```json { "scripts": { "test": "vitest", "test:api": "vitest --config vitest.config.api.ts", "test:functions": "vitest --config vitest.config.functions.ts", "test:all": "vitest && vitest --config vitest.config.api.ts" } } ``` ## コントラクトテスト: VitestによるRustエンジン mdx-formatterの実例:VitestスイートがRustフォーマッティングエンジンのコントラクトテストとして機能します。Node.jsラッパーがRustバイナリを呼び出し、Vitestが出力の期待値との一致を検証します: ```typescript // tests/contract.test.ts describe("Rust formatter contract", () => { it("formats basic MDX correctly", () => { const input = "# Hello\nSome text here"; const result = execSync(`echo '${input}' | ./target/release/formatter`, { encoding: "utf-8", }); expect(result.trim()).toBe("# Hello\n\nSome text here"); }); }); ``` コントラクトテストにより、上位レベルの言語からバイナリの動作を検証できます。Vitestスイートが仕様として機能し、Rustエンジンの動作が変わるとコントラクトテストがキャッチします。 ## 冪等性テスト フォーマッターやトランスフォーマーにとって強力な不変条件:操作を2回適用した結果は、1回適用した結果と同じでなければなりません。 ```typescript // tests/idempotency.test.ts const FIXTURES_DIR = join(__dirname, "fixtures"); describe("idempotency", () => { const fixtures = readdirSync(FIXTURES_DIR).filter((f) => f.endsWith(".mdx") ); for (const fixture of fixtures) { it(`is idempotent for ${fixture}`, () => { const input = readFileSync(join(FIXTURES_DIR, fixture), "utf-8"); const firstPass = format(input); const secondPass = format(firstPass); expect(firstPass).toBe(secondPass); }); } }); ``` ## Miniflare + D1/R2 インテグレーションテスト zudo-pattern-genの実例:MiniflareによるローカルD1データベースとR2ストレージを使ったCloudflare Workersのテスト: ```typescript // tests/integration.test.ts describe("Worker with D1", () => { let mf: Miniflare; beforeAll(async () => { mf = new Miniflare({ modules: true, script: `export default { async fetch(req, env) { /* ... */ } }`, d1Databases: ["DB"], r2Buckets: ["STORAGE"], }); // Run migrations const db = await mf.getD1Database("DB"); await db.exec(` CREATE TABLE IF NOT EXISTS patterns ( id TEXT PRIMARY KEY, name TEXT NOT NULL, data TEXT NOT NULL ) `); }); it("stores and retrieves a pattern", async () => { const resp = await mf.dispatchFetch("http://localhost/api/patterns", { method: "POST", body: JSON.stringify({ name: "test", data: "{}" }), }); expect(resp.status).toBe(201); const getResp = await mf.dispatchFetch("http://localhost/api/patterns"); const patterns = await getResp.json(); expect(patterns).toHaveLength(1); expect(patterns[0].name).toBe("test"); }); }); ``` Miniflareは同じWorkersランタイムをローカルで実行するため、インテグレーションテストは本番環境の動作に近くなります。D1とR2のバインディングを組み合わせることで、デプロイなしに完全なデータフローをテストできます。 --- # レベル1: ユニット/ロジックテスト > Source: /pj/zudo-test/ja/docs/testing-levels/level-1-unit-tests ## レベル1がテストするもの レベル1のテストは**純粋なロジック**を検証します。DOM、ブラウザAPI、視覚的レンダリングに触れることなく、入力を受け取り出力を返す関数が対象です。 典型的な対象: - ユーティリティ関数(文字列操作、日付フォーマット、計算) - データ変換(APIレスポンスのマッピング、正規化) - 状態リデューサーとセレクター - バリデーションロジック - ビジネスルール ## ツール | ツール | ユースケース | |-------|------------| | **vitest** | モダンなプロジェクト、Viteベース、高速HMR | | **jest** | レガシープロジェクト、CRA、広くサポート | ## 例 ```typescript // utils/format-price.ts return `$${(cents / 100).toFixed(2)}`; } ``` ```typescript // utils/format-price.test.ts describe("formatPrice", () => { it("formats cents to dollar string", () => { expect(formatPrice(1299)).toBe("$12.99"); }); it("handles zero", () => { expect(formatPrice(0)).toBe("$0.00"); }); it("handles single-digit cents", () => { expect(formatPrice(5)).toBe("$0.05"); }); }); ``` ## TDDサイクル ロジックの変更には標準的なTDDサイクルに従います: 1. 期待される動作を記述する失敗するテストを書く 2. テストをパスさせる最小限のコードを実装する 3. テストがグリーンであることを確認する 4. 必要に応じてリファクタリングする 5. 各動作について繰り返す ## ブラインドスポット レベル1のテストは**視覚的なすべてに対して盲目**です。以下を検出できません: - 要素が画面にレンダリングされるかどうか - CSSの問題(overflow、z-index、opacity、display:none) - レイアウトの問題(DOMに存在するが表示されない要素) - ブラウザ固有のレンダリング動作 - ユーザーインタラクションフロー レベル1は、バグがレンダリングではなくロジックにあると確信している場合に適切な選択です。表示や視覚的な正しさに疑問がある場合は、レベル4または5にエスカレーションしてください。 ## レベル1を使用するタイミング | シナリオ | レベル1は適切か? | |---------|-----------------| | 関数が誤った値を返す | はい | | データ変換が不正確 | はい | | バリデーションが有効な入力を拒否する | はい | | 要素が画面に表示されない | いいえ -- レベル4/5を使用 | | レイアウトが正しくない | いいえ -- レベル5を使用 | | クリックハンドラーが動作しない | いいえ -- レベル2または4を使用 | --- # テスト時の必須行動 > Source: /pj/zudo-test/ja/docs/decision-guide/required-behavior ## 5つのルール フロントエンドコードに取り組むすべてのAIエージェントは、テスト時にこの5つのルールに従う必要があります: ### ルール1: 最初にテスト計画を宣言する テストを書いたり検証を実行したりする前に、以下を明示します: - **何を**テストするか - **どのレベル**を使用するか - **なぜ**そのレベルが適切か ``` Test plan: - Testing: notification banner visibility after CSS fix - Level: 5 (verify-ui + screenshot) - Reason: This is a visual bug — the element exists in DOM but user reports it's not visible. Need to check computed styles. ``` テスト計画を宣言することで、習慣的にレベル1をデフォルトにしてしまうよくある間違いを防げます。意識的なレベル選択を強制します。 ### ルール2: テストレベルを目標に合わせる テストレベルは実際に検証しているものと一致しなければなりません: | 検証している内容 | 最低限使用すべきレベル | |----------------|---------------------| | 関数が正しい値を返す | レベル1 | | コンポーネントが正しい要素をレンダリングする | レベル2 | | ビルド出力に期待されるコンテンツが含まれる | レベル3 | | ユーザーフローがブラウザで動作する | レベル4 | | 何かが視覚的に正しい | レベル5 | レベル5の問題にレベル1を使用しないでください。テストはパスしますが、バグは残ります。 ### ルール3: 下位レベルがパスしても問題が解消しない場合はエスカレーションする テストがパスしたにもかかわらず、ユーザーが問題が修正されていないと言う場合: 1. 同じテストを再実行**しない** 2. ユーザーにキャッシュのクリアを提案**しない** 3. 次のテストレベルにエスカレーション**する** 4. すでにレベル4の場合、レベル5にエスカレーションする 5. レベル5の場合、より深く調査する(祖先要素、スタッキングコンテキストなどをチェック) **「ブラウザキャッシュをクリアしてください」や「ハードリフレッシュしてください」を解決策として提案しないでください。** ユーザーがまだ壊れていると言う場合、コードがまだ壊れています。キャッシュのせいにするのではなく、実際の原因を調査してください。 ### ルール4: UI/CSSはデフォルトでレベル5 以下に関わるタスク: - CSSの変更 - レイアウトの変更 - 表示の問題 - 視覚的な外観 - レスポンシブデザイン - 間隔、色、フォント はレベル5の検証をデフォルトにすべきです。下位レベルはCSSの正しさを検証する構造的能力がありません。 ### ルール5: テストしなかったものを報告する テスト後、ブラインドスポットを明示的に述べます: ``` Verification complete: - Tested: computed styles confirm banner has display:block, opacity:1, and parent has overflow:visible - Screenshot: banner is visible at top of page - NOT tested: responsive behavior at mobile breakpoints - NOT tested: animation transition timing ``` この透明性により、追加のテストが必要かどうかをユーザーが判断できます。 ## サマリーチェックリスト 修正完了を宣言する前に: - [ ] テスト前にテスト計画を宣言した - [ ] テストレベルが変更の性質と一致している - [ ] テストがパスしたがユーザーが失敗を報告した場合、次のレベルにエスカレーションした - [ ] CSS/ビジュアルの変更にはレベル5を使用した - [ ] テストしたものとしなかったものを報告した --- # Playwrightパターン > Source: /pj/zudo-test/ja/docs/real-world-patterns/playwright-patterns ## CI安全テスト vs @interactiveテストの分割 すべてのE2EテストがCIで実行できるわけではありません。キーボードショートカット、クリップボードアクセス、デスクトップ固有のインタラクションを必要とするテストにはタグを付けて分割します: ```typescript // e2e/basic-navigation.spec.ts -- runs in CI test("loads the home page", async ({ page }) => { await page.goto("/"); await expect(page.locator("h1")).toBeVisible(); }); ``` ```typescript // e2e/keyboard-shortcuts.spec.ts -- only runs locally test("@interactive Ctrl+S saves document", async ({ page }) => { await page.goto("/editor"); await page.keyboard.press("Control+KeyS"); await expect(page.locator(".save-indicator")).toHaveText("Saved"); }); ``` ```typescript // playwright.config.ts projects: [ { name: "ci", testMatch: /.*\.spec\.ts/, testIgnore: /.*@interactive.*/, }, { name: "interactive", testMatch: /.*@interactive.*\.spec\.ts/, }, ], }); ``` CIでは `npx playwright test --project=ci` を実行し、フルキーボード/クリップボードテストが必要な場合はローカルで `npx playwright test --project=interactive` を実行します。 ## コンソールエラーモニタリング Playwrightのテストフィクスチャを拡張して、コンソールエラー時に自動的にテストを失敗させます: ```typescript // e2e/fixtures.ts consoleErrors: async ({ page }, use) => { const errors: string[] = []; page.on("console", (msg) => { if (msg.type() === "error") { errors.push(msg.text()); } }); page.on("pageerror", (error) => { errors.push(error.message); }); await use(errors); // Assert no console errors after each test expect(errors).toEqual([]); }, }); ``` ```typescript // e2e/app.spec.ts test("home page has no console errors", async ({ page, consoleErrors }) => { await page.goto("/"); await page.waitForLoadState("networkidle"); // consoleErrors assertion happens automatically in fixture teardown }); ``` ## CI画像インターセプトによる高速化 CIでは、大きな画像のネットワークリクエストがテストを遅くします。インターセプトして小さなプレースホルダーに置き換えます: ```typescript // e2e/fixtures.ts page: async ({ page }, use) => { // Intercept image requests in CI if (process.env.CI) { await page.route("**/*.{png,jpg,jpeg,webp,gif}", (route) => { route.fulfill({ status: 200, contentType: "image/png", // 1x1 transparent PNG body: Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ), }); }); } await use(page); }, }); ``` zmodのこのパターンにより、画像アセットのネットワークレイテンシーが排除され、CI E2Eテスト時間が40%削減されました。 ## 本番ビルド検証 devサーバーではなく、本番ビルドに対してテストを実行します。これによりビルド固有の問題をキャッチできます: ```typescript // playwright.config.ts webServer: { command: "npm run build && npm run preview", port: 4173, reuseExistingServer: !process.env.CI, }, use: { baseURL: "http://localhost:4173", }, }); ``` ```typescript // e2e/production.spec.ts test("production build serves all pages", async ({ page }) => { const urls = ["/", "/docs", "/about", "/contact"]; for (const url of urls) { const response = await page.goto(url); expect(response?.status()).toBe(200); } }); test("production build has no broken links", async ({ page }) => { await page.goto("/"); const links = await page.locator("a[href^='/']").all(); for (const link of links) { const href = await link.getAttribute("href"); if (href) { const response = await page.goto(href); expect(response?.status()).toBe(200); } } }); ``` ## シャードCIラン 大規模なテストスイートの場合、複数のCIランナーにシャードします: ```yaml # .github/workflows/e2e.yml jobs: e2e: strategy: matrix: shard: [1/4, 2/4, 3/4, 4/4] steps: - uses: actions/checkout@v4 - run: npx playwright install --with-deps - run: npx playwright test --shard=${{ matrix.shard }} - uses: actions/upload-artifact@v4 if: failure() with: name: playwright-report-${{ strategy.job-index }} path: playwright-report/ ``` ## フロントエント専用E2Eのためのモックバックエンドアダプター 実際のバックエンドから独立してフロントエンドの動作をテストする場合: ```typescript // e2e/mocks/backend-adapter.ts await page.route("**/api/**", async (route) => { const url = new URL(route.request().url()); const mocks: Record = { "/api/user": { id: 1, name: "Test User", email: "test@example.com" }, "/api/settings": { theme: "dark", language: "en" }, "/api/documents": [ { id: 1, title: "Doc 1" }, { id: 2, title: "Doc 2" }, ], }; const mockData = mocks[url.pathname]; if (mockData) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mockData), }); } else { await route.continue(); } }); } ``` ```typescript // e2e/frontend.spec.ts test.beforeEach(async ({ page }) => { await mockBackend(page); }); test("displays user name from mock API", async ({ page }) => { await page.goto("/dashboard"); await expect(page.locator(".user-name")).toHaveText("Test User"); }); ``` モックバックエンドはフロントエント中心のテストには最適ですが、実際のAPIに対するインテグレーションテストの代わりにはなりません。両方を使用してください:UIの動作にはモック、データフローには実際のAPI。 --- # レベル2: DOMベースのコンポーネントテスト > Source: /pj/zudo-test/ja/docs/testing-levels/level-2-dom-tests ## レベル2がテストするもの レベル2のテストは、**シミュレートされたDOM環境でのコンポーネントの動作**を検証します。実際のブラウザなしで、コンポーネントが正しい要素をレンダリングし、ユーザーイベントに応答し、状態を正しく更新することを確認できます。 典型的な対象: - コンポーネントのレンダリング(正しい要素を出力するか?) - 条件付き表示(propsや状態に基づいて表示/非表示するか?) - イベントハンドラー(クリックで正しい動作がトリガーされるか?) - propsに基づく動作 - コンポーネント間の連携(親子コンポーネントの通信) ## ツール | ツール | 役割 | |-------|------| | **vitest** | テストランナー | | **jsdom** または **happy-dom** | シミュレートされたブラウザDOM環境 | | **@testing-library/react** | DOMクエリとユーザーイベントシミュレーション | | **@testing-library/preact** | Preactプロジェクト向け | ## セットアップ DOM環境を使用するようvitestを設定します: ```typescript // vitest.config.ts test: { environment: "jsdom", // or "happy-dom" }, }); ``` **happy-dom** はほとんどのケースで jsdom より高速です。より広いブラウザAPI互換性が必要な場合は jsdom を使用してください。 ## 例 ```tsx // components/Toggle.tsx const [on, setOn] = useState(false); return ( setOn(!on)}> {label}: {on ? "ON" : "OFF"} ); } ``` ```tsx // components/Toggle.test.tsx describe("Toggle", () => { it("renders with OFF state", () => { render(); expect(screen.getByText("Sound: OFF")).toBeTruthy(); }); it("toggles to ON on click", async () => { render(); await userEvent.click(screen.getByRole("button")); expect(screen.getByText("Sound: ON")).toBeTruthy(); }); }); ``` ## ブラインドスポット レベル2のテストは実際のブラウザではなく**シミュレートされた**DOMを使用します。以下を検出できません: - CSSの効果(DOMにはCSSエンジンがない) - 視覚的レイアウト(CSSによりDOMに存在しても非表示の要素) - ブラウザ固有のレンダリング - スクロール動作 - アニメーションとトランジションの状態 - 算出スタイル 重要なギャップ:jsdomのツリーに要素が存在していても(レベル2はパス)、CSSにより画面上では完全に非表示になっている場合があります(レベル5ならこれをキャッチ)。 ## レベル2を使用するタイミング | シナリオ | レベル2は適切か? | |---------|-----------------| | コンポーネントが誤ったテキストをレンダリング | はい | | propsが正しく渡されない | はい | | クリックハンドラーが状態を更新しない | はい | | 要素が存在するが表示されない | いいえ -- レベル5を使用 | | CSSレイアウトが壊れている | いいえ -- レベル5を使用 | | 複数ページのナビゲーションフロー | いいえ -- レベル4を使用 | --- # Tauriアプリのテスト > Source: /pj/zudo-test/ja/docs/real-world-patterns/tauri-testing ## WebKit専用ルール TauriはすべてのプラットフォームでレンダリングエンジンとしてWebKitを使用します。TauriフロントエンドのPlaywright E2Eテストを書く際は、常にWebKitに対してテストしてください: ```typescript // playwright.config.ts projects: [ { name: "webkit", use: { ...devices["Desktop Safari"] }, }, ], }); ``` PlaywrightでTauriフロントエンドをChromiumやFirefoxに対してテスト**しないでください**。本番アプリはWebKitを使用するため、他のエンジンに対するテストは誤った確信を与えます。Chromiumでテストがパスしても、Tauriウィンドウで動作するとは限りません。 ## コアクレートパターン プラットフォーム非依存のビジネスロジックを、Tauri依存のない別のRustクレートに抽出します。このクレートはTauriアプリケーションコンテキストなしで標準的な `cargo test` でテストできます: ``` src-tauri/ Cargo.toml # depends on core + tauri src/ main.rs # Tauri setup, commands commands.rs # #[tauri::command] handlers core/ Cargo.toml # no Tauri dependency src/ lib.rs settings.rs # pure Rust logic file_ops.rs # file operations transforms.rs # data transforms ``` ```toml # core/Cargo.toml [package] name = "myapp-core" version = "0.1.0" edition = "2021" [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" # No tauri dependency here ``` ```rust // core/src/settings.rs use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct Settings { pub theme: String, pub font_size: u32, } impl Settings { pub fn with_theme(mut self, theme: &str) -> Self { self.theme = theme.to_string(); self } } #[cfg(test)] mod tests { use super::*; #[test] fn test_with_theme() { let settings = Settings { theme: "light".to_string(), font_size: 14, }; let updated = settings.with_theme("dark"); assert_eq!(updated.theme, "dark"); assert_eq!(updated.font_size, 14); } } ``` コアクレートパターンにより、完全なTauriアプリをビルドせずにCIで `cargo test` を実行できます。高速で信頼性が高く、ロジックバグを早期にキャッチします。 ## バックエンドブリッジモックアダプターパターン Tauriアプリでは、フロントエンドはIPCコマンドを通じてRustバックエンドと通信します。フロントエンドテストでは、このブリッジをモックします: ```typescript // src/adapters/backend.ts -- the real adapter getSettings(): Promise; saveSettings(settings: Settings): Promise; readFile(path: string): Promise; } async getSettings() { return invoke("get_settings"); }, async saveSettings(settings) { return invoke("save_settings", { settings }); }, async readFile(path) { return invoke("read_file", { path }); }, }; ``` ```typescript // src/adapters/mock-backend.ts -- for testing overrides: Partial = {} ): BackendAdapter { return { async getSettings() { return { theme: "dark", fontSize: 14 }; }, async saveSettings() {}, async readFile() { return "mock file content"; }, ...overrides, }; } ``` ```typescript // In the app entry point const backend = import.meta.env.MODE === "test" ? createMockBackend() : tauriBackend; ``` ## 8ステップエスカレーションラダー Tauriアプリの場合、エスカレーションラダーは標準の5レベルを超えて拡張されます: | ステップ | メソッド | キャッチするもの | |---------|--------|---------------| | 1 | コアクレートの`cargo test` | 純粋なRustロジックバグ | | 2 | Vitestユニットテスト | フロントエンドロジックバグ | | 3 | Vitest + jsdomコンポーネントテスト | コンポーネント動作バグ | | 4 | Playwright WebKit(devサーバー) | フロントエンドレンダリングバグ | | 5 | Playwright WebKit(本番ビルド) | ビルド固有のフロントエンドバグ | | 6 | verify-ui + headless-browser | フロントエンドのCSS/ビジュアルバグ | | 7 | Tauriデブモードの手動テスト | IPC連携バグ | | 8 | Tauri本番ビルドの手動テスト | 完全なアプリパッケージングバグ | ステップ1-6は自動化可能でCIに含めるべきです。ステップ7-8は完全なTauriアプリケーションが必要で、通常は手動またはディスプレイサーバーを備えた専用CIが必要です。 ### ステップバイステップガイド **ステップ1-3: 高速、自動化可能、ブラウザ不要** ```bash # Step 1: Rust core logic cd core && cargo test # Step 2: Frontend unit tests pnpm vitest --project unit # Step 3: Frontend component tests pnpm vitest --project component ``` **ステップ4-6: ブラウザが必要、ただし自動化可能** ```bash # Step 4: E2E against dev server (WebKit only) pnpm dev & npx playwright test --project=webkit # Step 5: E2E against production build pnpm build pnpm preview & npx playwright test --project=webkit # Step 6: Visual verification verify-ui --url http://localhost:4173 --selector ".app" --check "display: flex" ``` **ステップ7-8: 完全なTauriアプリが必要** ```bash # Step 7: Tauri dev mode pnpm tauri dev # Manual testing in the actual Tauri window # Step 8: Production build pnpm tauri build # Test the built .dmg / .msi / .AppImage ``` ## Tauriプロジェクトの CI設定 ```yaml # .github/workflows/test.yml jobs: rust-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: cd core && cargo test frontend-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - run: pnpm install - run: pnpm vitest run e2e-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - run: pnpm install - run: npx playwright install webkit --with-deps - run: pnpm build && pnpm preview & - run: npx playwright test --project=webkit ``` CIでのTauri E2EテストにはWebKitの依存関係が必要です。Ubuntuの場合、`npx playwright install webkit --with-deps` が必要です。macOSランナーではWebKitはすでに利用可能です。 --- # レベル3: ビルド出力検証 > Source: /pj/zudo-test/ja/docs/testing-levels/level-3-build-output ## レベル3がテストするもの レベル3のテストは**ビルド出力**を検証します。ソースコードを直接テストする代わりに、ビルドを実行してその結果を検査します。 典型的な対象: - SSG(静的サイト生成)出力HTML - テンプレートのレンダリング結果 - バンドラ出力(正しいチャンク、コード分割) - 生成された設定ファイル - ビルド時のデータ変換(MDXコンパイル、コンテンツコレクション) ## ツール | ツール | 役割 | |-------|------| | **vitest** | テストランナー、ファイルの読み取りとアサーション | | **fs/path** | ビルド出力を読むためのNode.jsファイルシステムAPI | | **cheerio** | HTML出力のパースとクエリ | ## 例: SSG出力検証 ```typescript // tests/build-output.test.ts const DIST = join(__dirname, "../dist"); describe("build output", () => { it("generates index.html with correct title", () => { const html = readFileSync(join(DIST, "index.html"), "utf-8"); expect(html).toContain("My Site"); }); it("generates sitemap.xml", () => { const sitemap = readFileSync(join(DIST, "sitemap.xml"), "utf-8"); expect(sitemap).toContain(" { const pages = ["index.html", "about/index.html", "docs/index.html"]; for (const page of pages) { const content = readFileSync(join(DIST, page), "utf-8"); expect(content).toContain(""); } }); }); ``` ## 例: MDXフォーマッターのコントラクトテスト mdx-formatterの実例パターン:VitestスイートがRustフォーマッティングエンジンをフィクスチャファイルで実行し、出力を比較するコントラクトテスト: ```typescript // tests/format.test.ts describe("mdx formatting", () => { it("is idempotent", () => { const input = readFixture("sample.mdx"); const first = format(input); const second = format(first); expect(first).toBe(second); }); }); ``` **冪等性テスト**はフォーマッターやトランスフォーマーにとって強力な不変条件です:操作を2回適用した結果は、1回適用した結果と同じでなければなりません。 ## ブラインドスポット レベル3のテストはファイルの内容を検証しますが、ランタイム動作は検証しません。以下を検出できません: - JavaScriptのランタイムエラー - クライアントサイドのハイドレーション問題 - 視覚的レンダリングの問題 - ブラウザAPIとのインタラクション - ページロード後に動的に読み込まれるコンテンツ ## レベル3を使用するタイミング | シナリオ | レベル3は適切か? | |---------|-----------------| | SSGページがビルドに含まれていない | はい | | 出力のHTML構造が誤っている | はい | | バンドルが大きすぎる/チャンクが不正 | はい | | ブラウザでのハイドレーションミスマッチ | いいえ -- レベル4を使用 | | ページがブラウザで空白表示 | いいえ -- レベル4/5を使用 | | CSSが正しく適用されていない | いいえ -- レベル5を使用 | --- # バックエンド & Node.js テスト > Source: /pj/zudo-test/ja/docs/real-world-patterns/backend-testing ## フロントエンド・バックエンド分離の哲学 著者のアプローチ:フロントエンドが得意で、バックエンド実装はAIに委任する。これによりテスト戦略が重要になります -- バックエンドのコードを自分で書いていない場合、テストがそれが正しく動作しているかの主要な検証手段となります。 重要な原則は**関心の分離がテスト可能なバックエンドを実現する**ということです。フロントエンドとバックエンドが適切に分離されている場合: - 各レイヤーを独立してテストできる - フロントエンドテストはMSWライクなモックを使用してバックエンドの可用性から切り離す - バックエンドテストはブラウザを必要とせず、実際のまたはエミュレートされたインフラストラクチャに対して実行する - 一方のレイヤーの変更が他方のレイヤーのテストを壊さない この分離は単なるアーキテクチャ上の美点ではなく、AI支援バックエンド開発を実現可能にするものです。AIが実装を書き、テストがそれが実際に動作することを検証します。 ## Cloudflare Functions と Miniflare Cloudflare Workers/FunctionsでD1(SQLite)データベースとR2オブジェクトストレージを使用するプロジェクトでは、Miniflareがインテグレーションテスト用のローカルエミュレーションを提供します。 ### テスト環境のセットアップ 実際のD1とR2バインディングを持つ分離されたテスト環境を起動するヘルパーを作成します: ```ts // test/helpers/test-env.ts const mf = new Miniflare({ modules: true, script: "", d1Databases: ["DB"], r2Buckets: ["BUCKET"], }); const env = await mf.getBindings(); // Run SQL migrations const migration = readFileSync("migrations/0001_init.sql", "utf-8"); const db = env.DB as D1Database; await db.exec(migration); return { mf, env, db, bucket: env.BUCKET as R2Bucket }; } ``` ### テストデータファクトリ 合理的なデフォルト値を持つファクトリ関数を使用してテストデータを作成します: ```ts // test/helpers/factories.ts return { id: crypto.randomUUID(), name: "Test Project", createdAt: new Date().toISOString(), ...overrides, }; } ``` ### フルCRUDライフサイクルテスト エミュレートされたインフラストラクチャに対して、完全なCRUD(作成・読み取り・更新・削除)サイクルをテストします: ```ts describe("Projects API", () => { let env: any; let mf: any; beforeEach(async () => { ({ env, mf } = await createTestEnv()); }); it("creates and retrieves a project", async () => { const createRes = await app.request( new Request("http://localhost/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "My Project" }), }), {}, env, ); expect(createRes.status).toBe(201); const created = await createRes.json(); const getRes = await app.request( new Request(`http://localhost/api/projects/${created.id}`), {}, env, ); expect(getRes.status).toBe(200); const fetched = await getRes.json(); expect(fetched.name).toBe("My Project"); }); }); ``` ここでの重要なパターンは`app.request(req, {}, env)`です -- これはMiniflareが提供するバインディングを使用してHonoアプリを直接呼び出し、HTTPを完全にバイパスします。 ### 別のVitest設定 バックエンドテストは`environment: 'node'`を持つ独自のVitest設定が必要です: ```ts // vitest.config.backend.ts test: { environment: "node", include: ["test/backend/**/*.test.ts"], testTimeout: 10000, }, }); ``` ## HTTP APIテスト ライブまたはデプロイされたエンドポイント(ステージング、プレビュー、本番)に対してテストするには、直接HTTPリクエストを使用します。 ### 環境ベースのURL切り替え ```ts // test/helpers/api-client.ts function getBaseUrl(): string { if (process.env.TEST_API_URL) { return process.env.TEST_API_URL; } if (process.env.CF_PAGES_URL) { return process.env.CF_PAGES_URL; } return "http://localhost:8787"; } const BASE_URL = getBaseUrl(); return fetch(`${BASE_URL}${path}`, { headers: { Authorization: `Bearer ${process.env.TEST_API_TOKEN}`, }, }); } ``` ### 破壊的テストのガード データを変更するテストは本番環境ではスキップすべきです: ```ts const isProduction = process.env.TEST_ENV === "production"; describe("Admin API", () => { it.skipIf(isProduction)("deletes all test data", async () => { const res = await apiGet("/api/admin/reset-test-data"); expect(res.status).toBe(200); }); }); ``` ### ネットワークタイムアウト HTTPテストはユニットテストよりも長いタイムアウトが必要です: ```ts // vitest.config.http.ts test: { environment: "node", include: ["test/http/**/*.test.ts"], testTimeout: 30000, }, }); ``` ## モックを使ったHTTPクライアントテスト HTTPリクエストを行うコード(APIクライアント、認証フロー)をテストする場合、グローバルレベルで`fetch`をモックします。 ### `vi.stubGlobal`パターン ```ts describe("ApiClient", () => { const fetchMock = vi.fn(); beforeEach(() => { vi.stubGlobal("fetch", fetchMock); }); afterEach(() => { vi.restoreAllMocks(); }); it("sends auth header", async () => { fetchMock.mockResolvedValueOnce( new Response(JSON.stringify({ ok: true }), { status: 200 }), ); const client = new ApiClient({ token: "test-token" }); await client.get("/api/data"); expect(fetchMock).toHaveBeenCalledWith( expect.stringContaining("/api/data"), expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer test-token", }), }), ); }); }); ``` ### 認証フローテスト モックレスポンスを連鎖させて、トークンリフレッシュとリトライロジックをテストします: ```ts it("retries with refreshed token on 401", async () => { // First call returns 401 fetchMock.mockResolvedValueOnce(new Response(null, { status: 401 })); // Token refresh succeeds fetchMock.mockResolvedValueOnce( new Response(JSON.stringify({ token: "new-token" }), { status: 200 }), ); // Retry with new token succeeds fetchMock.mockResolvedValueOnce( new Response(JSON.stringify({ data: "success" }), { status: 200 }), ); const client = new ApiClient({ token: "old-token" }); const result = await client.get("/api/data"); expect(result.data).toBe("success"); expect(fetchMock).toHaveBeenCalledTimes(3); }); ``` ## ファイルシステムテスト ファイルの読み書きを行うNode.jsツールでは、分離のために一時ディレクトリを使用します。 ### 一時ディレクトリパターン ```ts describe("FileProcessor", () => { let tempDir: string; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "test-")); }); afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); it("processes and writes output file", () => { const inputPath = join(tempDir, "input.txt"); const outputPath = join(tempDir, "output.txt"); writeFileSync(inputPath, "hello world"); processFile(inputPath, outputPath); expect(readFileSync(outputPath, "utf-8")).toBe("HELLO WORLD"); }); }); ``` 重要なポイント: - `mkdtempSync`はテスト実行ごとにユニークなディレクトリを作成する -- 衝突がない - `afterEach`のクリーンアップにより、テスト間で残存ファイルがないことを保証する - すべてのパスは`tempDir`からの相対パス -- テストが実際のファイルシステムに触れることはない ## バックエンドテストの重要な原則 1. **vitest設定で`environment: 'node'`を使用する**。`jsdom`ではなく。バックエンドコードにDOMは不要です。 2. **フロントエンドとバックエンドテストで別々のvitest設定**を使用する。異なる環境、異なるタイムアウト、そして多くの場合異なるセットアップファイルが必要です。 3. **テストデータ用のヘルパーファクトリ**を使用する。テストデータをインラインでハードコードせず、合理的なデフォルト値とオーバーライドを持つファクトリ関数を使用します。 4. **設定切り替えに環境変数**を使用する。ローカル、プレビュー、本番URLの切り替えにはハードコードではなく`process.env`を使用します。 5. **破壊的テストを`it.skipIf()`でガード**する。データを削除したり状態をリセットしたりするテストは、本番環境では絶対に実行すべきではありません。 6. **ネットワークテストには長いタイムアウト**を設定する。デフォルトの5秒タイムアウトはHTTPインテグレーションテストには短すぎます。30秒以上を使用します。 --- # レベル4: E2Eブラウザテスト > Source: /pj/zudo-test/ja/docs/testing-levels/level-4-e2e-browser ## レベル4がテストするもの レベル4のテストは**実際のブラウザ**(またはヘッドレスブラウザ)で実行され、ページロードからインタラクションを経て最終状態に至るまでの完全なユーザーフローを検証します。シミュレートされた環境では見逃すランタイムエラー、ナビゲーションの問題、インタラクションのバグをキャッチします。 典型的な対象: - 完全なユーザーワークフロー(ログイン、フォーム送信、ナビゲーション) - クライアントサイドルーティング - API連携(モックまたは実際のエンドポイント) - コンソールエラーの検出 - ページ間のインタラクション - 動的コンテンツの読み込み ## ツール | ツール | 役割 | |-------|------| | **Playwright** | マルチブラウザ対応の完全なE2Eテストフレームワーク | | **headless-browser** | ページヘルスチェック用のクイック検証スクリプト | ## 例: Playwright E2E ```typescript // e2e/navigation.spec.ts test("navigates from home to docs", async ({ page }) => { await page.goto("/"); await page.click('a[href="/docs"]'); await expect(page).toHaveURL("/docs"); await expect(page.locator("h1")).toHaveText("Documentation"); }); test("no console errors on page load", async ({ page }) => { const errors: string[] = []; page.on("console", (msg) => { if (msg.type() === "error") errors.push(msg.text()); }); await page.goto("/"); expect(errors).toHaveLength(0); }); ``` ## 例: ヘッドレスブラウザによるクイックチェック フルテストスイートを使わない迅速な検証には、headless-browserでスクリーンショットを撮影しエラーをチェックします: ```bash # Quick page health check node headless-check.js --url http://localhost:3000 --screenshot # Check for console errors node headless-check.js --url http://localhost:3000 --console-errors ``` ## コンソールエラーモニタリング 本番プロジェクトからの強力なパターン:E2Eテスト中にコンソール出力を監視して予期しないエラーをキャッチします: ```typescript // e2e/fixtures.ts page: async ({ page }, use) => { const errors: string[] = []; page.on("console", (msg) => { if (msg.type() === "error") { errors.push(msg.text()); } }); await use(page); if (errors.length > 0) { throw new Error( `Console errors detected:\n${errors.join("\n")}` ); } }, }); ``` ## ブラインドスポット レベル4のテストは実際のブラウザレンダリングとインタラクションしますが、通常は視覚的な外観ではなく**DOMの状態**をアサートします。以下を見逃す可能性があります: - 微妙なCSSの問題(1ピクセルのズレ、色の微妙な違い) - 技術的には表示されているが視覚的に重なっている要素 - フォントレンダリングの違い - レスポンシブレイアウトのブレークポイント - 算出スタイルの値 レベル4は「要素が存在してインタラクション可能である」ことを確認します。「要素が正しく見える」ことを確認するには、レベル5にエスカレーションしてください。 ## レベル4を使用するタイミング | シナリオ | レベル4は適切か? | |---------|-----------------| | ページナビゲーションが壊れている | はい | | フォーム送信が失敗する | はい | | 動的コンテンツが読み込まれない | はい | | ページにコンソールエラーがある | はい | | 要素の配置が間違っている | 部分的 -- レベル5がより適切 | | CSSの色/間隔が間違っている | いいえ -- レベル5を使用 | --- # レベル5: 決定論的 + 視覚的検証 > Source: /pj/zudo-test/ja/docs/testing-levels/level-5-visual-verification ## レベル5がテストするもの レベル5は**決定論的な算出スタイルのアサーション**と**視覚的なスクリーンショット検証**を組み合わせて、すべての下位レベルが見逃すバグをキャッチします。UI/CSS作業における最も信頼度の高い検証方法です。 2つの補完的なツール: - **verify-ui** -- 実行中のページから算出CSS値を抽出し、正確な値をアサート(LLMの解釈なし) - **headless-browser** -- 視覚的な比較とインタラクションテスト用のスクリーンショットを撮影 ## 両方が必要な理由 算出スタイルのチェックだけでは、要素間のインタラクション(重なり、スタッキングコンテキスト)から生じる視覚的な問題を見逃す可能性があります。スクリーンショットだけでは、確証バイアスの可能性があるLLMの解釈に依存します。組み合わせることで以下を提供します: | アプローチ | 強み | 弱み | |----------|------|------| | verify-ui | 決定論的、正確な値 | 視覚的な構成を見られない | | headless-browser | 完全な視覚的結果を見られる | LLMの解釈にバイアスの可能性 | | 両方の組み合わせ | 決定論的 + 視覚的 | 最小限のブラインドスポット | ## verify-uiの例 ```bash # Check computed styles of an element verify-ui --url http://localhost:3000 \ --selector ".hero-title" \ --check "font-size: 48px" \ --check "color: rgb(255, 255, 255)" \ --check "display: block" ``` ```bash # Verify visibility verify-ui --url http://localhost:3000 \ --selector ".notification-banner" \ --check "display: block" \ --check "opacity: 1" \ --check "visibility: visible" ``` ## このレベルがキャッチする典型的な失敗 典型的なシナリオを考えてみます: 1. AIエージェントが通知バナーコンポーネントを追加する 2. ユニットテストでコンポーネントがレンダリングされることを確認する(レベル1パス) 3. DOMテストで要素がツリーに存在することを確認する(レベル2パス) 4. しかし親コンテナに `overflow: hidden` と `height: 0` がある 5. バナーはDOMに存在するが完全に非表示 レベル5ならこれをキャッチします: ```bash # verify-ui would reveal: # .notification-banner parent has height: 0px, overflow: hidden verify-ui --url http://localhost:3000 \ --selector ".notification-container" \ --check "height: auto" \ --check "overflow: visible" ``` headless-browserのスクリーンショットにより、バナーが表示されていないことを視覚的に確認できます。 ## headless-browserによる視覚的検証 ```bash # Full-page screenshot node headless-check.js --url http://localhost:3000 --screenshot --full-page # Screenshot of specific viewport node headless-check.js --url http://localhost:3000 \ --screenshot --width 375 --height 812 # iPhone viewport ``` ## 統合されたワークフロー UI/CSSの変更に対する推奨検証ワークフロー: 1. CSS/レイアウトの変更を行う 2. **verify-ui**を実行して正確な算出値をアサートする 3. **headless-browser**でスクリーンショットを撮影して視覚的に確認する 4. verify-uiがパスしてもスクリーンショットが正しくない場合、スタッキング/コンポジションの問題を調査する 5. スクリーンショットが正しいがverify-uiが失敗する場合、期待値を更新する この2段階のアプローチにより、偽陽性(スクリーンショットは良好だが値が不正)と偽陰性(値は正しいが視覚的なコンポジションが壊れている)の両方を排除します。 ## ブラインドスポット レベル5のブラインドスポットは最小限ですが、いくつか残ります: - OSごとのフォントレンダリングの違い - サブピクセルレンダリングのバリエーション - アニメーションのタイミング(フレーム途中の状態) - テストブラウザに存在しないブラウザ固有の癖 ## レベル5を使用するタイミング | シナリオ | レベル5は適切か? | |---------|-----------------| | CSSの変更が反映されない | はい | | DOMに存在するのに要素が表示されない | はい | | レイアウトの間隔が正しくない | はい | | 色やfont-sizeが不正確 | はい | | レスポンシブブレークポイントの問題 | はい | | 下位レベルのテストがパスした後にユーザーが「まだ壊れている」と報告 | はい | --- # test-wisdom スキル > Source: /pj/zudo-test/ja/docs/overview/test-wisdom-skill `test-wisdom`スキルは、このサイトのすべてのフロントエンドテストドキュメント記事をインデックスするClaude Codeスキルです。AI コーディングエージェントが開発中に関連するテストパターンやテクニックを素早く参照できるようにします。 ## 機能 このスキルは、テストの概念をドキュメント記事にマッピングするドキュメントインデックスを管理します。呼び出されると、関連する記事を見つけて読み、推奨されるパターンを適用します。 ドキュメントインデックスは、`src/content/docs/` および `src/content/docs-ja/`(日本語版)配下のすべてのMDX記事から生成されます。各`.mdx`ファイルには`title`と`description`フィールドを持つYAMLフロントマターがあり、適切な記事の特定に役立ちます。 ## インストール セットアップスクリプトを実行して、スキルを作成しグローバルClaude Codeスキルディレクトリにシンボリックリンクします: ```bash pnpm run setup:doc-skill ``` これにより`.claude/skills/test-wisdom/`にスキルが作成され、`~/.claude/skills/test-wisdom`にシンボリックリンクされます。 ## 使い方 ### ルックアップモード(デフォルト) 任意のClaude Codeセッションで、トピックキーワードを指定してスキルを呼び出します: ``` /test-wisdom vitest patterns /test-wisdom playwright e2e /test-wisdom testing level escalation ``` スキルはドキュメントから関連する記事を見つけて読み、コード記述時にテストパターンを適用します。 ### アップデートモード(`-u` / `--update`) テストに関する新しい情報があり、このリポジトリのドキュメントを追加または更新したい場合は、`-u`フラグを使用します: ``` /test-wisdom -u vitest /test-wisdom --update playwright patterns ``` アップデートモードでは、スキルはClaudeに以下を案内します: 1. 学んだことやドキュメント化したい内容を尋ねる 2. 既存のドキュメントを検索して関連記事を見つける 3. 新しい`.mdx`ファイルを作成するか、既存のファイルを更新する 4. `docs-ja/`配下の対応する日本語翻訳を更新する 5. `pnpm format:md`を実行して新規/変更ファイルをフォーマットする ## スキル構造 ``` .claude/skills/test-wisdom/ SKILL.md # 生成されたスキル定義 docs/ # src/content/docs/ へのシンボリックリンク docs-ja/ # src/content/docs-ja/ へのシンボリックリンク ``` `SKILL.md`ファイルはセットアップスクリプトによって生成されます。スキルのメタデータとClaude Codeがドキュメントをどのように使用するかの指示が含まれています。 ## 動作原理 `/test-wisdom <トピック>`を呼び出すと、Claude Codeは: 1. トピックに基づいて`docs/`ディレクトリから関連する記事を見つける 2. 必要な特定の記事のみを読む -- すべての記事を一度に読み込むことはない 3. 記事の情報を質問への回答に適用する 4. ソース記事のパスを提示し、さらなる参照ができるようにする 日本語ドキュメントは`docs-ja/`配下で利用可能です。日本語で作業する場合や日本語コンテンツを求める場合、スキルは`docs-ja/`の記事を優先します。