E2Eテストメイン記事

著者: ClaudeMix Team |
E2Eテストメイン記事のサムネイル

【パフォーマンス計測対象記事】この記事は性能測定の基準値として使用されます。
十分なテキスト量を確保するため、詳細な解説を含めています。

この記事では、ブログのカテゴリ・タグフィルター機能をPlaywrightでテストする実装例を紹介します。また、アクセス制御のリダイレクト挙動や、タグが指定されていない場合のエッジケースについても解説します。

FilterPanelのE2Eテスト設計

フィルター機能のE2Eテストでは、以下の項目を確認する必要があります:

  1. カテゴリフィルタの動作確認

    • CategorySelectorでカテゴリを選択
    • 決定ボタンクリック後、該当カテゴリの記事のみが表示される
    • URLパラメータが正しく設定される
  2. タグフィルタの動作確認

    • TagGrid内のタグボタンを選択
    • 複数タグ選択時はAND条件で絞り込まれる
    • 決定ボタンクリック後、該当タグを持つ記事のみが表示される
  3. 複合フィルタの動作確認

    • カテゴリとタグを同時に選択
    • 両方の条件を満たす記事のみが表示される

アクセス制御とリダイレクトの検証

アクセス制御が設定されているカテゴリでは、未認証ユーザーがアクセスした際のリダイレクト挙動を確認する必要があります。
posts-spec.yamlaccess_rules 設定に基づき、適切なページ(/register など)へリダイレクトされることをテストします。

タグなし記事のエッジケース

タグが空の配列 [] である記事のハンドリングも重要です。

  • 記事一覧で正しく表示されるか
  • タグフィルタ適用時に適切に除外されるか
    これらの挙動を保証することで、システムの堅牢性が高まります。

テストデータの管理

テストデータは spec.yaml で一元管理します。これにより、テストコードの保守性が向上します。

// tests/e2e/utils/loadSpec.ts の使用例
import { getTestArticlesByTag, getTestArticlesByCategory } from './loadSpec';

// Playwrightタグを持つテスト記事を取得
const playwrightArticles = getTestArticlesByTag('Playwright');

// Tutorials & Use Cases カテゴリのテスト記事を取得
const tutorialArticles = getTestArticlesByCategory(3);

フィルタ適用後の検証ポイント

  1. URL確認 : URLクエリパラメータが正しく設定されているか
  2. 記事表示確認 : フィルター条件に一致する記事のみが表示されているか
  3. 記事カウント確認 : 表示件数が期待値と一致しているか
  4. パネル閉じる確認 : フィルター適用後、FilterPanelが自動的に閉じるか

実装例

test('タグフィルタ選択確認', async ({ page }) => {
  await page.goto('/blog');

  // FilterPanelを開く
  await page.click('[data-testid="filter-toggle-button"]');

  // Playwrightタグを選択
  await page.click('[data-testid="tag-button"][data-tag="Playwright"]');

  // 決定ボタンをクリック
  await page.click('[data-testid="filter-submit-button"]');

  // URLパラメータを確認
  await expect(page).toHaveURL(/tags=Playwright/);

  // フィルター適用後の記事を確認
  const articles = await page.locator('[data-testid="post-card"]').all();
  expect(articles.length).toBeGreaterThan(0);
});

ベストプラクティス

E2Eテストを書く際のベストプラクティスをいくつか紹介します。

1. セレクタの選定

テスト用のセレクタは data-testid を使用することを推奨します。これにより、CSSクラスやDOM構造の変更に影響されにくいテストを書くことができます。

2. 待機処理の適切な使用

waitForTimeout の使用は避け、代わりに waitForSelectorwaitForLoadState を使用しましょう。

3. テストの独立性

各テストは他のテストに依存せず、単独で実行できるようにしましょう。テスト間で状態を共有しないことが重要です。

クライアントサイド JS 負荷 (Mermaid)

classDiagram
    class Engine {
        +String baseUrl
        +String target
        +int retry
        +boolean summary
        +run() void
        +parseArgs() Object
        +detectBaseUrl() String
    }
    class PathConfig {
        +String name
        +String path
        +Record thresholds
    }
    class PSI_API {
        +measurePage(url, apiKey) Promise
    }
    class ReportGenerator {
        +generate(results) String
        +saveToFile(report) void
    }
    Engine --> PathConfig : uses
    Engine --> PSI_API : calls
    Engine --> ReportGenerator : triggers

    class BlogController {
        +getPosts() List
        +getPost(slug) Post
    }
    class Post {
        +String title
        +String content
        +List tags
    }
    BlogController ..> Post : manages

シンタックスハイライト負荷 (TypeScript/JSX)

import React, { useState, useEffect, useMemo } from 'react';
import { useLoaderData, Link } from '@remix-run/react';

// 100行以上のコードブロックをシミュレートするための複雑なコンポーネント
export const BenchmarkComponent = () => {
  const [count, setCount] = useState(0);
  const [data, setData] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      // 重い処理のシミュレーション
      const result = await new Promise(resolve => {
        setTimeout(() => {
          resolve(Array.from({ length: 100 }).map((_, i) => ({
            id: i,
            value: `Item ${i}`,
            timestamp: Date.now()
          })));
        }, 500);
      });
      setData(result as any[]);
      setLoading(false);
    };
    fetchData();
  }, []);

  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      expensiveValue: Math.sqrt(item.id) * Math.random()
    })).filter(item => item.id % 2 === 0);
  }, [data]);

  const handleIncrement = () => {
    setCount(prev => prev + 1);
    console.log('Incremented:', count);
  };

  const handleReset = () => {
    setCount(0);
  };

  if (loading) {
    return <div className="p-4 animate-pulse">Loading complex data...</div>;
  }

  return (
    <div className="p-6 border rounded-lg shadow-sm bg-white dark:bg-gray-800">
      <h2 className="text-2xl font-bold mb-4">Benchmark Analysis Dashboard</h2>
      <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
        <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded">
          <p className="text-sm text-blue-600 dark:text-blue-400">Interaction Count</p>
          <p className="text-3xl font-mono">{count}</p>
        </div>
        <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded">
          <p className="text-sm text-green-600 dark:text-green-400">Items Processed</p>
          <p className="text-3xl font-mono">{processedData.length}</p>
        </div>
      </div>

      <div className="flex space-x-2 mb-8">
        <button
          onClick={handleIncrement}
          className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition"
        >
          Increment
        </button>
        <button
          onClick={handleReset}
          className="px-4 py-2 border border-gray-300 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition"
        >
          Reset
        </button>
      </div>

      <div className="overflow-x-auto">
        <table className="w-full text-left border-collapse">
          <thead>
            <tr className="border-b dark:border-gray-700">
              <th className="py-2 px-4 font-semibold">ID</th>
              <th className="py-2 px-4 font-semibold">Value</th>
              <th className="py-2 px-4 font-semibold">Expensive Calc</th>
              <th className="py-2 px-4 font-semibold">Status</th>
            </tr>
          </thead>
          <tbody>
            {processedData.slice(0, 10).map(item => (
              <tr key={item.id} className="border-b dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50">
                <td className="py-2 px-4 font-mono">{item.id}</td>
                <td className="py-2 px-4">{item.value}</td>
                <td className="py-2 px-4 text-sm text-gray-500">{item.expensiveValue.toFixed(4)}</td>
                <td className="py-2 px-4">
                  <span className="inline-block w-2 h-2 rounded-full bg-green-500 mr-2" />
                  Active
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <div className="mt-8 prose dark:prose-invert max-w-none">
        <h3>Extended Code Documentation</h3>
        <p>
          This component demonstrates several performance-intensive React patterns,
          including complex state management, side effects with asynchronous data fetching,
          and memoized computations over large datasets.
        </p>
        <ul>
          <li><strong>Hooks:</strong> useState, useEffect, useMemo are all employed to test hydration and re-rendering performance.</li>
          <li><strong>Data Size:</strong> 100 items are generated and processed, with a subset displayed in a table.</li>
          <li><strong>Interactivity:</strong> Buttons trigger state updates that necessitate re-renders and potential re-computations.</li>
        </ul>
      </div>

      <div className="mt-12 text-xs text-gray-400">
        <h4>Performance Metric Logs</h4>
        <pre>
          {JSON.stringify({
            testId: 'benchmark-001',
            metrics: ['LCP', 'FID', 'CLS', 'TBT', 'TTI'],
            environment: 'Production-Like',
            timestamp: new Date().toISOString(),
            notes: 'Automated performance regression target'
          }, null, 2)}
        </pre>
      </div>

      {/* Adding some redundant functions to increase line count */}
      {(() => {
        const calculateFactorial = (n: number): number => {
          if (n <= 1) return 1;
          return n * calculateFactorial(n - 1);
        };
        const generateFibo = (n: number): number[] => {
          const res = [0, 1];
          for (let i = 2; i < n; i++) {
            res.push(res[i - 1] + res[i - 2]);
          }
          return res;
        };
        const complexMath = (a: number, b: number) => {
          return Math.pow(a, b) / Math.sqrt(a + b) * Math.sin(a) * Math.cos(b);
        };
        return (
          <div className="mt-4 p-2 bg-gray-100 rounded text-[10px] font-mono">
            Debug: {calculateFactorial(5)} | {generateFibo(5).join(',')} | {complexMath(10, 20).toFixed(2)}
          </div>
        );
      })()}
    </div>
  );
};

export default BenchmarkComponent;

まとめ

フィルター機能のE2Eテストでは、spec.yamlからテストデータを読み込み、実際のユーザー操作をシミュレートすることで、堅牢なテストを実現できます。また、アクセス制御やタグなし記事といったエッジケースの検証を自動化することで、リグレッションを早期に発見可能になります。

参考リソース

さらに詳しく学びたい方は、以下のリソースを参照してください: