Self-healing Tests with LLM — AI 自動修壞掉的 Selector

「跑 E2E 的時間 20%、修 selector 的時間 80%」是 QA 痛點 Top 3。Self-healing test 用 LLM 自動找出新 selector、把維護時間砍掉 70%。這篇給你完整工具地圖 + 自建方案。

為什麼 selector 會壞

flowchart LR
    UI[UI 改動] --> Sel{Selector 壞?}
    Sel --> B1["class 改名"]
    Sel --> B2["DOM 結構重組"]
    Sel --> B3["元素位置變"]
    Sel --> B4["A/B test 新版本"]
    Sel --> B5["CSS-in-JS hash 變"]

    Sel --> Fix[QA 修]
    Fix --> Time["每週 5-8 小時"]
    Time --> Cost["年 $20-30k 工時成本"]

    style Time fill:#ef4444,color:#fff
    style Cost fill:#ef4444,color:#fff

Self-healing 工作流

flowchart TD
    Run[跑 test] --> Find[找 selector]
    Find --> Q{找到?}
    Q -->|是| Pass[Pass]
    Q -->|否| LLM[LLM 介入]
    LLM --> Analyze[分析周圍 DOM]
    Analyze --> Suggest[建議新 selector]
    Suggest --> Try[Try 新 selector]
    Try --> Q2{有元素?}
    Q2 -->|是| Continue[繼續 test + 標記]
    Q2 -->|否| Fail[Fail]
    Continue --> Alert[Slack 通知 QA]
    Alert --> Review[人工 review 是 cosmetic 還 bug]

    style Continue fill:#10b981,color:#fff
    style Alert fill:#f59e0b,color:#fff
    style Fail fill:#ef4444,color:#fff

關鍵:self-heal 後必須 alert + review、不能靜默修復、否則會掩蓋真 bug。

商用工具比較

工具 起跳價 強項 弱項
Mabl $200/月 完整 E2E platform、UX 好 鎖定平台
Functionize 企業 高 AI 比重
Testim (Tricentis) $450/月 大廠資源 老牌 UX
Reflect.run $69/月 便宜起跳 功能少
BugBug 免費起跳 起步 OK 規模化弱

自建 Self-healing — Playwright + Claude

核心思路

1. 寫 wrapper 攔截 selector 失敗
2. 失敗時拿 DOM snapshot + 原 selector
3. 餵給 LLM「依照原 selector 意圖、在新 DOM 找替代」
4. 試新 selector
5. 成功 → 繼續 + 記 log
6. 失敗 → 報錯

實作範例

import { Page, Locator } from '@playwright/test';
import Anthropic from '@anthropic-ai/sdk';

const claude = new Anthropic();

async function smartLocator(page: Page, selector: string, hint?: string): Promise<Locator> {
  try {
    const loc = page.locator(selector);
    await loc.waitFor({ timeout: 3000 });
    return loc;
  } catch {
    // Selector 找不到 → 啟動 self-healing
    console.warn(`⚠️ Selector "${selector}" not found, trying AI...`);

    const html = await page.content();
    const truncated = html.slice(0, 30000);  // 控制 token

    const response = await claude.messages.create({
      model: 'claude-sonnet-4-6',
      max_tokens: 200,
      messages: [{
        role: 'user',
        content: `原 selector: ${selector}
${hint ? `用途: ${hint}` : ''}

下面是當前 HTML。請建議一個能找到「同一元素」的新 selector。只回 selector 字串、不要解釋。

HTML:
${truncated}`,
      }],
    });

    const newSelector = response.content[0].text.trim();
    console.log(`🤖 AI 建議新 selector: ${newSelector}`);

    // 記到 healing log
    await logHeal(selector, newSelector);

    return page.locator(newSelector);
  }
}

// 使用
test('login with self-healing', async ({ page }) => {
  await page.goto('/login');
  const loginBtn = await smartLocator(page, '#login-btn', '登入按鈕');
  await loginBtn.click();
});

healing log

import fs from 'fs';

async function logHeal(oldSel: string, newSel: string) {
  const log = {
    at: new Date().toISOString(),
    old_selector: oldSel,
    new_selector: newSel,
    file: __filename,
  };
  fs.appendFileSync('healing.log', JSON.stringify(log) + '\n');

  // 也發 Slack
  await fetch(process.env.SLACK_WEBHOOK!, {
    method: 'POST',
    body: JSON.stringify({
      text: `🤖 Self-heal: ${oldSel} → ${newSel}`,
    }),
  });
}

Slack alert 範例

🤖 Self-heal triggered

File: tests/login.spec.ts
Old selector: #login-btn
New selector: button[data-testid="submit-login"]
Time: 2026-06-17 14:32

Action needed:
- Was this a planned UI change? → Update test
- Was this a regression? → Report bug

跟 Page Object Model 並存

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}

  // 用 smart locator wrapper
  get emailInput() { return smartLocator(this.page, '[name=email]', 'Email 輸入框'); }
  get passwordInput() { return smartLocator(this.page, '[name=password]', 'Password 輸入框'); }
  get submitBtn() { return smartLocator(this.page, 'button[type=submit]', '送出按鈕'); }

  async login(email: string, password: string) {
    await (await this.emailInput).fill(email);
    await (await this.passwordInput).fill(password);
    await (await this.submitBtn).click();
  }
}

POM 給結構、self-healing 給彈性 = 完美組合

CI 整合

name: E2E with Self-Healing
on: [pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx playwright test
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

      # 收集 healing log
      - if: always()
        uses: actions/upload-artifact@v4
        with:
          name: healing-log
          path: healing.log

      # 多於 X 次 heal → fail PR
      - if: always()
        run: |
          COUNT=$(wc -l < healing.log)
          if [ $COUNT -gt 10 ]; then
            echo "❌ Too many self-heals ($COUNT) — likely UI mass change"
            exit 1
          fi

何時該用 / 不該用

flowchart TD
    Q{該用 self-healing?} --> Q1{E2E 數量?}
    Q1 -->|< 30| No1[手動修還可]
    Q1 -->|30-100| Yes1[Yes - 自建]
    Q1 -->|100+| Yes2[Yes - Mabl 等商用]

    Q --> Q2{UI 改動頻率?}
    Q2 -->|穩定| No2[CP 值低]
    Q2 -->|每週改| Yes3[強烈推薦]

    Q --> Q3{安全要求?}
    Q3 -->|金融 / 醫療| Caution[小心 - 別靜默修復]
    Q3 -->|一般 SaaS| Free[放心用]

    style Yes1 fill:#10b981,color:#fff
    style Yes2 fill:#10b981,color:#fff
    style Yes3 fill:#10b981,color:#fff
    style Caution fill:#f59e0b,color:#fff

反模式

flowchart TD
    Anti[Self-healing 反模式] --> A1["完全信任 AI、靜默修復"]
    Anti --> A2["沒設 budget cap、API 費用爆"]
    Anti --> A3["把 selector 全部寫死等 AI 修"]
    Anti --> A4["不分析 heal log、不更新 test"]
    Anti --> A5["在金融 / 醫療系統用、沒 audit"]
    Anti --> A6["取代 review、QA 看都不看"]

    style A1 fill:#ef4444,color:#fff
    style A2 fill:#ef4444,color:#fff
    style A3 fill:#ef4444,color:#fff
    style A4 fill:#ef4444,color:#fff
    style A5 fill:#ef4444,color:#fff
    style A6 fill:#ef4444,color:#fff

成本控制

// 每次 heal 約 $0.02-0.05(Claude Sonnet)
// 100 個 E2E、每月 50 次 heal = $2.50/月 — 划算

// 但無 budget cap 會炸
const MAX_HEALS_PER_RUN = 20;
let healCount = 0;

async function smartLocator(...) {
  if (healCount >= MAX_HEALS_PER_RUN) {
    throw new Error(`Heal budget exceeded (${MAX_HEALS_PER_RUN})`);
  }
  healCount++;
  // ...
}

給 QA 的 5 句

  1. Self-healing 解的是 cosmetic 變化、不是業務 bug
  2. 永遠 alert + log、不要靜默修復
  3. POM + Self-healing = E2E 維護地獄解決組合
  4. 設 budget cap、API 費用會爆
  5. 金融 / 醫療要 audit log、別當救命神器

最後

Self-healing test 是 2026 後 QA 維護生產力的 game-changer。自動化 selector 飄移、保留人類判斷力。從 30 個 case 自建 wrapper 起步、3 個月後你會把維護時間從每週 8 小時砍到 2 小時。

延伸: - Page Object Model 實戰 - Flaky Test 排雷指南 - AI 共存的 QA 工具箱