GEMINI LABEN
NANOLITE — Nano Banana 2 Liteが登場しました。Googleで最も速く、最もコスト効率の高いGemini Imageモデルで、軽量な画像生成を安く回したい用途に向いていますOMNIFLASH — Gemini Omni Flashがpublic previewになりました。ネイティブにマルチモーダルなモデルで、企業や開発者が独自の動的な動画ワークフローを構築できますAGENTS — Managed Agentsが拡張されました。background: trueでサーバー側の非同期実行とポーリング、リモートMCPサーバー連携、対話をまたぐ認証情報のリフレッシュに対応しますMEMORY — Memory BankのIngestEvents APIが一般提供になりました。イベントの取り込みとメモリ生成を分離し、コンテンツを継続的にストリームできますTHROUGHPUT — Provisioned Throughputで、同一モデル・同一リージョンに対して最大7件の保留オーダーを提出できるようになりましたDEPRECATE — 画像生成モデルは8月17日に、Gemini Enterprise Agent PlatformのGrok 4.1系は8月20日に停止される予定ですNANOLITE — Nano Banana 2 Liteが登場しました。Googleで最も速く、最もコスト効率の高いGemini Imageモデルで、軽量な画像生成を安く回したい用途に向いていますOMNIFLASH — Gemini Omni Flashがpublic previewになりました。ネイティブにマルチモーダルなモデルで、企業や開発者が独自の動的な動画ワークフローを構築できますAGENTS — Managed Agentsが拡張されました。background: trueでサーバー側の非同期実行とポーリング、リモートMCPサーバー連携、対話をまたぐ認証情報のリフレッシュに対応しますMEMORY — Memory BankのIngestEvents APIが一般提供になりました。イベントの取り込みとメモリ生成を分離し、コンテンツを継続的にストリームできますTHROUGHPUT — Provisioned Throughputで、同一モデル・同一リージョンに対して最大7件の保留オーダーを提出できるようになりましたDEPRECATE — 画像生成モデルは8月17日に、Gemini Enterprise Agent PlatformのGrok 4.1系は8月20日に停止される予定です
記事一覧/開発ツール
開発ツール/2026-03-30上級

Gemini API × Playwright — AI駆動E2Eテスト自動生成システムの構築ガイド

Gemini APIとPlaywrightを組み合わせ、WebアプリのE2Eテストを自動生成・保守するシステムを構築する上級ガイド。ページ解析からテストコード生成、CI/CD統合まで網羅。

gemini-api279playwrighte2e-testingtest-automationai-testingci-cd5

なぜ AI でE2Eテストを自動生成するのか

E2Eテスト(エンドツーエンドテスト)は、Webアプリケーションの品質を保証するうえで最も信頼性の高いテスト手法の一つです。しかし現実には、テストコードの作成コストが高く、UIの変更に伴う保守作業が開発チームの大きな負担となっています。

Gemini API の強力なマルチモーダル解析能力と、Playwright のブラウザ自動操作を組み合わせることで、この課題を根本から解決できます。ここではWebページのスクリーンショットやHTML構造を Gemini に渡し、テストシナリオの策定からテストコードの生成、さらにはUI変更時の自動修復(セルフヒーリング)までを実現するシステムを構築します。

この記事で扱う技術スタックは以下の通りです。

  • Gemini 2.5 Pro: ページ解析・テストコード生成の中核エンジン
  • Playwright: クロスブラウザE2Eテスト実行フレームワーク
  • Node.js / TypeScript: システム全体の実装言語
  • GitHub Actions: CI/CDパイプラインへの統合

対象読者は、Playwright の基本的な使い方を知っている中〜上級のフロントエンド/QAエンジニアです。

アーキテクチャ概要

システム全体は以下の3つのレイヤーで構成されます。

1. ページ解析レイヤー(Page Analyzer)

Playwright でターゲットページにアクセスし、スクリーンショットの撮影、DOM構造の抽出、アクセシビリティツリーの取得を行います。これらのデータを Gemini API に送信し、ページの構成要素と操作可能なインタラクションを特定します。

2. テスト生成レイヤー(Test Generator)

Gemini が解析結果をもとに、Playwright のテストコードを TypeScript で生成します。単なるスナップショットテストではなく、ユーザーフローに沿った意味のあるテストシナリオを自動で組み立てます。

3. テスト実行・修復レイヤー(Test Runner & Self-Healer)

生成されたテストを実行し、失敗した場合は Gemini にエラー内容と最新のページ状態を渡して、テストコードの自動修正を試みます。これがセルフヒーリングの仕組みです。

環境構築

まず、プロジェクトをセットアップします。

mkdir gemini-e2e-generator && cd gemini-e2e-generator
npm init -y
npm install @google/generative-ai playwright typescript tsx
npx playwright install chromium
npx tsc --init

tsconfig.json を以下のように設定します。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

環境変数ファイル .env に API キーを設定します。

GEMINI_API_KEY=YOUR_GEMINI_API_KEY

ページ解析モジュールの実装

最初に、ターゲットページの情報を収集するモジュールを作成します。

// src/page-analyzer.ts
import { chromium, type Page, type Browser } from "playwright";
 
interface PageAnalysis {
  url: string;
  title: string;
  screenshot: Buffer;
  htmlStructure: string;
  accessibilityTree: string;
  interactiveElements: InteractiveElement[];
}
 
interface InteractiveElement {
  role: string;
  name: string;
  selector: string;
  type?: string;
}
 
export class PageAnalyzer {
  private browser: Browser | null = null;
 
  async init(): Promise<void> {
    this.browser = await chromium.launch({ headless: true });
  }
 
  async analyze(url: string): Promise<PageAnalysis> {
    if (!this.browser) throw new Error("Browser not initialized");
 
    const context = await this.browser.newContext({
      viewport: { width: 1280, height: 720 },
    });
    const page = await context.newPage();
    await page.goto(url, { waitUntil: "networkidle" });
 
    // スクリーンショット取得
    const screenshot = await page.screenshot({ fullPage: true });
 
    // HTML構造の簡略化抽出(トークン節約のため)
    const htmlStructure = await page.evaluate(() => {
      function summarize(el: Element, depth: number): string {
        if (depth > 4) return "";
        const tag = el.tagName.toLowerCase();
        const id = el.id ? `#${el.id}` : "";
        const cls = el.className
          ? `.${String(el.className).split(" ").slice(0, 2).join(".")}`
          : "";
        const text = el.textContent?.trim().slice(0, 50) || "";
        const children = Array.from(el.children)
          .map((c) => summarize(c, depth + 1))
          .filter(Boolean)
          .join("\n");
        const indent = "  ".repeat(depth);
        return `${indent}<${tag}${id}${cls}>${text ? ` "${text}"` : ""}\n${children}`;
      }
      return summarize(document.body, 0);
    });
 
    // アクセシビリティツリー取得
    const accessibilityTree = await this.getAccessibilityTree(page);
 
    // インタラクティブ要素の一覧取得
    const interactiveElements = await this.getInteractiveElements(page);
 
    await context.close();
 
    return {
      url,
      title: await page.title(),
      screenshot,
      htmlStructure,
      accessibilityTree,
      interactiveElements,
    };
  }
 
  private async getAccessibilityTree(page: Page): Promise<string> {
    const snapshot = await page.accessibility.snapshot();
    return JSON.stringify(snapshot, null, 2).slice(0, 8000);
  }
 
  private async getInteractiveElements(
    page: Page
  ): Promise<InteractiveElement[]> {
    return page.evaluate(() => {
      const selectors = [
        "button",
        "a[href]",
        'input:not([type="hidden"])',
        "select",
        "textarea",
        '[role="button"]',
        '[role="link"]',
        '[role="tab"]',
      ];
      const elements: InteractiveElement[] = [];
 
      for (const selector of selectors) {
        document.querySelectorAll(selector).forEach((el) => {
          const htmlEl = el as HTMLElement;
          elements.push({
            role: el.getAttribute("role") || el.tagName.toLowerCase(),
            name:
              el.getAttribute("aria-label") ||
              htmlEl.innerText?.trim().slice(0, 60) ||
              "",
            selector: generateSelector(el),
            type: el.getAttribute("type") || undefined,
          });
        });
      }
 
      function generateSelector(el: Element): string {
        if (el.id) return `#${el.id}`;
        const testId = el.getAttribute("data-testid");
        if (testId) return `[data-testid="${testId}"]`;
        const ariaLabel = el.getAttribute("aria-label");
        if (ariaLabel)
          return `${el.tagName.toLowerCase()}[aria-label="${ariaLabel}"]`;
        return `${el.tagName.toLowerCase()}:text("${(el as HTMLElement).innerText?.trim().slice(0, 30)}")`;
      }
 
      return elements.slice(0, 50);
    });
  }
 
  async close(): Promise<void> {
    await this.browser?.close();
  }
}

このモジュールのポイントは、Gemini に渡す情報を3種類用意している点です。スクリーンショット(視覚情報)、簡略化したHTML構造(構造情報)、アクセシビリティツリー(セマンティック情報)を組み合わせることで、Gemini がページの意図を正確に理解できるようになります。

Gemini によるテストコード生成

次に、収集したページ情報を Gemini API に送信し、テストコードを生成するモジュールを実装します。

// src/test-generator.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
import type { PageAnalysis } from "./page-analyzer";
 
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
 
const SYSTEM_PROMPT = `あなたはPlaywrightのE2Eテスト専門家です。
与えられたWebページの情報(スクリーンショット、HTML構造、アクセシビリティツリー、
インタラクティブ要素一覧)から、包括的なE2Eテストコードを生成してください。
 
## テストコード生成ルール
 
1. テストはPlaywright + TypeScript で記述する
2. セレクターは以下の優先順位で選択する:
   - data-testid 属性(最優先)
   - aria-label / role ベースのセレクター
   - テキストコンテンツベースのセレクター
   - CSS セレクター(最終手段)
3. 各テストには明確な説明(describe/it)を付ける
4. アサーションは具体的に: toBeVisible, toHaveText, toHaveURL 等を使い分ける
5. ページ遷移を伴うテストには適切な waitForURL / waitForLoadState を含める
6. ハッピーパス + エッジケース(空入力、不正値)の両方をカバーする
7. テストは独立して実行可能にする(テスト間の依存を作らない)
 
## 出力形式
 
TypeScript のコードブロックのみを出力してください。説明文は不要です。
ファイル名のコメントを先頭に含めてください(例: // tests/login.spec.ts)。`;
 
export async function generateTests(
  analysis: PageAnalysis
): Promise<string> {
  const model = genAI.getGenerativeModel({
    model: "gemini-2.5-pro",
    systemInstruction: SYSTEM_PROMPT,
  });
 
  const prompt = `以下のWebページに対するE2Eテストを生成してください。
 
## ページ情報
- URL: ${analysis.url}
- タイトル: ${analysis.title}
 
## HTML構造(簡略化)
${analysis.htmlStructure.slice(0, 6000)}
 
## アクセシビリティツリー
${analysis.accessibilityTree.slice(0, 4000)}
 
## インタラクティブ要素一覧
${JSON.stringify(analysis.interactiveElements, null, 2)}
 
上記の情報とスクリーンショットを総合的に分析し、
このページの主要なユーザーフローを網羅するテストコードを生成してください。`;
 
  const result = await model.generateContent([
    prompt,
    {
      inlineData: {
        mimeType: "image/png",
        data: analysis.screenshot.toString("base64"),
      },
    },
  ]);
 
  const text = result.response.text();
 
  // コードブロックからTypeScriptコードを抽出
  const codeMatch = text.match(/```typescript\n([\s\S]*?)```/);
  return codeMatch ? codeMatch[1].trim() : text;
}

ここで重要なのは、システムプロンプトでセレクターの優先順位を明示している点です。data-testid を最優先にすることで、UIの見た目が変わってもテストが壊れにくくなります。また、スクリーンショットをマルチモーダル入力として渡すことで、HTML構造だけでは判断できないビジュアルなレイアウトやコンテンツの意図を Gemini が把握できます。

セルフヒーリング(自動修復)機能の実装

E2Eテストが最もコストを消費するのは保守フェーズです。UIが少し変わるだけでテストが壊れ、手動で修正する必要があります。ここでは、テスト失敗時に Gemini が自動的にテストコードを修復するセルフヒーリング機能を実装します。

// src/self-healer.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
import { PageAnalyzer } from "./page-analyzer";
import * as fs from "fs/promises";
 
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
 
interface TestFailure {
  testFile: string;
  errorMessage: string;
  failedLine: number;
  pageUrl: string;
}
 
export class SelfHealer {
  private analyzer = new PageAnalyzer();
  private maxRetries = 3;
 
  async heal(failure: TestFailure): Promise<string | null> {
    await this.analyzer.init();
 
    try {
      // 現在のページ状態を再取得
      const currentState = await this.analyzer.analyze(failure.pageUrl);
 
      // 失敗したテストコードを読み込み
      const testCode = await fs.readFile(failure.testFile, "utf-8");
 
      const model = genAI.getGenerativeModel({
        model: "gemini-2.5-pro",
      });
 
      const prompt = `以下のPlaywrightテストが失敗しました。
ページの最新状態を分析し、テストコードを修正してください。
 
## 失敗情報
- エラー: ${failure.errorMessage}
- 失敗行: ${failure.failedLine}
 
## 現在のテストコード
\`\`\`typescript
${testCode}
\`\`\`
 
## 最新のページ状態
- URL: ${currentState.url}
- インタラクティブ要素:
${JSON.stringify(currentState.interactiveElements, null, 2)}
 
## アクセシビリティツリー(最新)
${currentState.accessibilityTree.slice(0, 4000)}
 
## 修正ルール
1. セレクターの変更のみで修正できる場合はセレクターだけ変更する
2. ページ構造が大幅に変わっている場合はテストロジックも更新する
3. 要素が完全に削除されている場合は該当テストをスキップ(test.skip)にする
4. 修正後のコード全体を出力すること
 
修正後のTypeScriptコードのみを出力してください。`;
 
      const result = await model.generateContent([
        prompt,
        {
          inlineData: {
            mimeType: "image/png",
            data: currentState.screenshot.toString("base64"),
          },
        },
      ]);
 
      const text = result.response.text();
      const codeMatch = text.match(/```typescript\n([\s\S]*?)```/);
      return codeMatch ? codeMatch[1].trim() : null;
    } finally {
      await this.analyzer.close();
    }
  }
 
  async healAndRetry(failure: TestFailure): Promise<boolean> {
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      console.log(
        `🔧 修復試行 ${attempt}/${this.maxRetries}: ${failure.testFile}`
      );
 
      const healed = await this.heal(failure);
      if (!healed) {
        console.log("❌ 修復コードを生成できませんでした");
        continue;
      }
 
      // 修復コードを書き込み
      await fs.writeFile(failure.testFile, healed, "utf-8");
      console.log("📝 テストコードを更新しました");
 
      // 再実行して成功するか確認
      const success = await this.runTest(failure.testFile);
      if (success) {
        console.log("✅ セルフヒーリング成功!");
        return true;
      }
    }
 
    console.log("❌ セルフヒーリング失敗 — 手動修正が必要です");
    return false;
  }
 
  private async runTest(testFile: string): Promise<boolean> {
    const { execSync } = await import("child_process");
    try {
      execSync(`npx playwright test ${testFile} --reporter=list`, {
        stdio: "pipe",
        timeout: 60000,
      });
      return true;
    } catch {
      return false;
    }
  }
}

セルフヒーリングのポイントは、失敗時に最新のページ状態を再取得してから修復を試みる点です。単にエラーメッセージだけを見るのではなく、ページの現在の構造をマルチモーダルに解析することで、UIリファクタリングやデザイン変更にも対応できます。

メインオーケストレーター

各モジュールを統合するメインスクリプトを作成します。

// src/index.ts
import { PageAnalyzer } from "./page-analyzer";
import { generateTests } from "./test-generator";
import { SelfHealer } from "./self-healer";
import * as fs from "fs/promises";
import * as path from "path";
 
interface GeneratorConfig {
  urls: string[];
  outputDir: string;
  selfHeal: boolean;
}
 
async function main(config: GeneratorConfig): Promise<void> {
  const analyzer = new PageAnalyzer();
  await analyzer.init();
 
  // 出力ディレクトリ作成
  await fs.mkdir(config.outputDir, { recursive: true });
 
  for (const url of config.urls) {
    console.log(`\n🔍 ページ解析中: ${url}`);
 
    try {
      // Step 1: ページ解析
      const analysis = await analyzer.analyze(url);
      console.log(
        `  📊 検出した要素: ${analysis.interactiveElements.length}個`
      );
 
      // Step 2: テストコード生成
      console.log("  🤖 Gemini でテストコード生成中...");
      const testCode = await generateTests(analysis);
 
      // Step 3: ファイル出力
      const slug = new URL(url).pathname
        .replace(/\//g, "-")
        .replace(/^-|-$/g, "")
        || "index";
      const testFile = path.join(
        config.outputDir,
        `${slug}.spec.ts`
      );
      await fs.writeFile(testFile, testCode, "utf-8");
      console.log(`  💾 テストファイル生成: ${testFile}`);
 
      // Step 4: テスト実行
      console.log("  ▶️ テスト実行中...");
      const { execSync } = await import("child_process");
      try {
        execSync(
          `npx playwright test ${testFile} --reporter=list`,
          { stdio: "inherit", timeout: 120000 }
        );
        console.log("  ✅ 全テストパス");
      } catch {
        if (config.selfHeal) {
          console.log("  🔧 セルフヒーリング開始...");
          const healer = new SelfHealer();
          await healer.healAndRetry({
            testFile,
            errorMessage: "テスト実行失敗",
            failedLine: 0,
            pageUrl: url,
          });
        }
      }
    } catch (error) {
      console.error(`  ❌ エラー: ${error}`);
    }
  }
 
  await analyzer.close();
  console.log("\n🏁 全ページの処理が完了しました");
}
 
// 実行
main({
  urls: [
    "https://example.com",
    "https://example.com/login",
    "https://example.com/dashboard",
  ],
  outputDir: "./tests/generated",
  selfHeal: true,
});

テスト生成の品質を高めるプロンプトテクニック

Gemini が生成するテストの品質を安定させるために、いくつかの実践的なテクニックがあります。

ページタイプ別のプロンプト分岐

ページの種類(ログインフォーム、一覧ページ、詳細ページなど)を事前に分類し、それぞれに最適化されたプロンプトテンプレートを使い分けます。

// src/prompt-templates.ts
export const PROMPT_TEMPLATES: Record<string, string> = {
  form: `このページにはフォームが含まれています。以下のテストを生成してください:
- 正常系: 全フィールドに有効な値を入力して送信
- バリデーション: 必須フィールドを空にして送信、エラーメッセージの確認
- 境界値: 最大文字数、特殊文字の入力テスト
- アクセシビリティ: Tab キーでのフォーカス移動順序の確認`,
 
  list: `このページはリスト/一覧ページです。以下のテストを生成してください:
- ページネーション: 次ページ/前ページへの遷移
- ソート: 各カラムでのソート切り替え
- フィルター: フィルター適用と解除
- 空状態: データがない場合の表示確認
- 項目クリック: 各アイテムの詳細ページへの遷移`,
 
  auth: `このページは認証ページです。以下のテストを生成してください:
- 正常ログイン: 有効な認証情報での認証フロー
- エラーケース: 無効なパスワード、存在しないユーザー
- バリデーション: メールアドレス形式チェック
- パスワードリセット: リセットフローの導線確認
- セキュリティ: XSS入力に対する防御確認`,
};
 
export function detectPageType(
  analysis: PageAnalysis
): string {
  const { interactiveElements, htmlStructure } = analysis;
  const formCount = interactiveElements.filter(
    (e) => e.role === "input" || e.role === "textarea"
  ).length;
 
  if (htmlStructure.includes("login") ||
      htmlStructure.includes("sign-in")) {
    return "auth";
  }
  if (formCount >= 3) return "form";
  if (htmlStructure.includes("pagination") ||
      interactiveElements.length > 15) {
    return "list";
  }
  return "general";
}

Function Calling によるテスト構造の制御

Gemini の Function Calling 機能を使って、テストの構造を JSON スキーマで厳密に制御することもできます。

// src/structured-generator.ts
import { GoogleGenerativeAI, type FunctionDeclaration } from "@google/generative-ai";
 
const testSuiteSchema: FunctionDeclaration = {
  name: "create_test_suite",
  description: "Playwright E2E テストスイートを構造化形式で生成する",
  parameters: {
    type: "object",
    properties: {
      fileName: {
        type: "string",
        description: "テストファイル名(例: login.spec.ts)",
      },
      imports: {
        type: "array",
        items: { type: "string" },
        description: "必要なインポート文",
      },
      testSuites: {
        type: "array",
        items: {
          type: "object",
          properties: {
            description: {
              type: "string",
              description: "describe ブロックの説明",
            },
            beforeEach: {
              type: "string",
              description: "beforeEach のコード(任意)",
            },
            tests: {
              type: "array",
              items: {
                type: "object",
                properties: {
                  name: { type: "string" },
                  priority: {
                    type: "string",
                    enum: ["critical", "high", "medium", "low"],
                  },
                  steps: {
                    type: "array",
                    items: { type: "string" },
                  },
                },
                required: ["name", "priority", "steps"],
              },
            },
          },
          required: ["description", "tests"],
        },
      },
    },
    required: ["fileName", "imports", "testSuites"],
  },
};

この方法では、Gemini に自由形式でコードを生成させるのではなく、テストの構造を事前に定義したスキーマに沿って出力させます。これにより、出力の安定性が大幅に向上し、後処理でのコード組み立てが容易になります。

CI/CD パイプラインへの統合

本番運用では、GitHub Actions からテスト生成と実行を自動化するのが理想的です。

# .github/workflows/e2e-ai-tests.yml
name: AI-Generated E2E Tests
 
on:
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 6 * * 1" # 毎週月曜 6:00 UTC に定期実行
 
env:
  GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
 
jobs:
  generate-and-run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
 
      - name: Install dependencies
        run: npm ci
 
      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium
 
      - name: Start application
        run: |
          npm run build
          npm start &
          npx wait-on http://localhost:3000 --timeout 30000
 
      - name: Generate E2E tests with Gemini
        run: npx tsx src/index.ts
 
      - name: Run generated tests
        run: npx playwright test tests/generated/ --reporter=html
 
      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: e2e-report
          path: playwright-report/
 
      - name: Comment PR with results
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const report = fs.existsSync('test-results.json')
              ? JSON.parse(fs.readFileSync('test-results.json', 'utf8'))
              : { passed: 0, failed: 0 };
            const emoji = report.failed === 0 ? '✅' : '⚠️';
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `${emoji} **AI-Generated E2E Test Results**\n\n` +
                `- Passed: ${report.passed}\n` +
                `- Failed: ${report.failed}\n\n` +
                `Full report is available in the workflow artifacts.`
            });

PRが作成されるたびに、変更されたページに対するE2Eテストを自動生成・実行し、結果をPRコメントとして報告する仕組みです。週次の定期実行では、全ページに対するリグレッションテストを実施します。

コスト最適化戦略

Gemini API の利用コストを抑えるために、以下の3つの戦略を組み合わせます。

1. キャッシュの活用

ページ構造が変わっていない場合は、テストの再生成をスキップします。ページのHTML構造のハッシュ値を保存しておき、変更があった場合のみ Gemini を呼び出します。

import * as crypto from "crypto";
 
function computePageHash(analysis: PageAnalysis): string {
  const content = analysis.htmlStructure +
    JSON.stringify(analysis.interactiveElements);
  return crypto.createHash("sha256").update(content).digest("hex");
}
 
// 前回のハッシュと比較して変更があった場合のみ再生成
const currentHash = computePageHash(analysis);
const cachedHash = await loadCachedHash(url);
if (currentHash === cachedHash) {
  console.log("ページに変更なし — テスト再生成をスキップ");
  return;
}

2. モデルの使い分け

初回のテスト生成には Gemini 2.5 Pro を使い、セルフヒーリング(セレクター修正程度の軽微な変更)には Gemini 2.5 Flash を使うことで、コストを大幅に削減できます。

3. バッチ処理

複数ページのテスト生成を一度にまとめてリクエストすることで、APIコールの回数を抑えます。Gemini の Batch Processing API を活用すれば、さらに50%のコスト削減が可能です。

実践的なテスト生成例

実際にログインページを解析した場合の生成例を見てみましょう。

// tests/generated/login.spec.ts
// Gemini API によって自動生成されたテスト
 
import { test, expect } from "@playwright/test";
 
test.describe("Login Page", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/login");
  });
 
  test("should display login form with all required elements", async ({
    page,
  }) => {
    await expect(
      page.getByRole("heading", { name: /ログイン|Sign In/i })
    ).toBeVisible();
    await expect(page.getByLabel(/メール|Email/i)).toBeVisible();
    await expect(page.getByLabel(/パスワード|Password/i)).toBeVisible();
    await expect(
      page.getByRole("button", { name: /ログイン|Sign In/i })
    ).toBeVisible();
  });
 
  test("should show validation errors for empty submission", async ({
    page,
  }) => {
    await page.getByRole("button", { name: /ログイン|Sign In/i }).click();
    await expect(
      page.getByText(/必須|required/i)
    ).toBeVisible();
  });
 
  test("should show error for invalid credentials", async ({ page }) => {
    await page.getByLabel(/メール|Email/i).fill("test@example.com");
    await page.getByLabel(/パスワード|Password/i).fill("wrongpassword");
    await page.getByRole("button", { name: /ログイン|Sign In/i }).click();
    await expect(
      page.getByText(/認証に失敗|invalid|incorrect/i)
    ).toBeVisible();
  });
 
  test("should navigate to password reset page", async ({ page }) => {
    await page
      .getByRole("link", { name: /パスワードを忘れた|Forgot/i })
      .click();
    await expect(page).toHaveURL(/reset|forgot/);
  });
});

Gemini は、ページの言語設定に応じて日本語・英語の両方に対応したセレクターを自動的に生成します。正規表現パターンを使うことで、多言語サイトでも安定して動作するテストになっています。

まとめ

Gemini API のマルチモーダル解析能力と Playwright のブラウザ自動操作を組み合わせることで、E2Eテストの作成と保守を大幅に自動化できます。ページ解析 → テストコード生成 → 実行 → セルフヒーリングというサイクルを CI/CD に組み込むことで、開発チームはテストの記述ではなく、プロダクトの品質向上そのものに集中できるようになります。

本記事のコードは TypeScript で実装していますが、Python 版の Playwright と Gemini SDK でも同じアーキテクチャを再現できます。まずは小さなプロジェクトで試してみて、生成されるテストの品質を確認しながら、徐々に適用範囲を広げていくのがおすすめです。

AI駆動テストの技術的基盤

シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-04-08
Terraform × Gemini API 本番インフラ自動化 — Google Cloud 上でのAIアプリケーション IaC 設計パターン
TerraformでGemini APIを使うAIアプリの本番インフラを完全自動化。IAM・Cloud Run・Vertex AI・Secret Manager・CI/CDまで体系的に解説するIaC設計パターン集。
開発ツール2026-03-26
Gemini API × GitHub Actions で構築する AI コード品質自動監査パイプライン — PR レビュー・セキュリティスキャン・ドキュメント生成の統合ワークフロー
Gemini API と GitHub Actions を組み合わせて、PR の自動コードレビュー・セキュリティ脆弱性スキャン・ドキュメント自動生成を統合した AI 品質監査パイプラインの本番設計パターンを解説します。
開発ツール2026-07-11
同じナレーションを二度生成しない — TTS 音声を端末にキャッシュする鍵設計と失効の運用
瞑想・癒し系アプリのように同じ音声を何度も再生する場面で、Gemini TTS の出力そのものをキャッシュしてコストを実質ゼロに近づける設計です。テキストだけでは足りないキャッシュキーの組み立て方、サーバ生成キャッシュと端末キャッシュの二層構成、モデル停止期限への備えとしての失効ポリシーを、動くコードと実測値でまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →