---
title: Self-healing Tests with LLM — AI 自動修壞掉的 Selector
description: Self-healing test 完整指南。LLM 自動修壞掉的 selector、Mabl / Functionize 比較、自建方案、CI 整合、何時該用 / 不該用、與傳統 selector 策略對比。
category: ai-qa
tags: [self-healing, ai-selector, mabl, functionize, llm-tests]
date: 2026-06-17
faq:
  - q: Self-healing test 真的能取代 maintenance?
    a: 不能完全取代、但能砍 60-80% selector 維護時間。功能性 bug 還是要人 review、self-healing 只解「selector 改了」這類 cosmetic 變化。
  - q: 該自建還用 Mabl / Functionize?
    a: 50+ E2E test + 預算 → Mabl ($200-500/月起)。50 個以下 + 想學 → 自建（用 Playwright + Claude API）。Functionize 較貴、企業向。
  - q: Self-healing 會掩蓋真 bug 嗎？
    a: 會。如果按鈕「不見了」AI 卻找個類似元素點 → 沒人發現該 bug 已上線。對策：self-heal 後一律 alert + 人工 review、不要靜默修復。
  - q: 跟 Page Object Model 衝突嗎？
    a: 不衝突、是互補。POM 解「結構化」、self-healing 解「selector 飄移」。兩個一起用 = E2E 維護地獄解決。
---

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

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

## 為什麼 selector 會壞

```mermaid
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 工作流

```mermaid
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. 失敗 → 報錯
```

### 實作範例

```typescript
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

```typescript
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 並存

```typescript
// 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 整合

```yaml
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
```

## 何時該用 / 不該用

```mermaid
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
```

## 反模式

```mermaid
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
```

### 成本控制

```typescript
// 每次 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 實戰](/automation/page-object-model.html)
- [Flaky Test 排雷指南](/automation/flaky-test-debugging.html)
- [AI 共存的 QA 工具箱](/ai-qa/ai-toolkit-for-qa.html)
