iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0

昨天講完 Chrome DevTools MCP(以下簡稱 CDP)能拿到 Network 和 Console,測試的驗證力跟我想像的比較靠近

所以想嘗試能不能順便幫我把 pytest 腳本寫出來呢?


AI Chrome DevTools 跑完測試,順便生腳本

CDP 跑測試的過程中,其實已經掌握了「哪個元素、什麼操作、什麼預期結果」。這些資訊丟掉太可惜。

所以我在 Prompt 裡加了兩段:

- 最後執行完成後,更新至 md_result/result.md
  1. 幫我將有成功的測試步驟轉換成正確元素的 Xpath,以便於之後生產測試腳本使用
    - Xpath 策略邏輯為
      1. 有明確 id 先找 id(是 DOM 屬性上的 id,並非 Chrome MCP 生成的 id)
      2. 使用完整文字組合搭配相對路徑
      3. 完整 Xpath 絕對路徑
  2. 讀取測試結果檔案,生成一個 pytest 腳本並且自動執行,
     如果有錯誤要能自行修復後更新回腳本中,最多 3 次

整條鏈路變成這樣:

https://ithelp.ithome.com.tw/upload/images/20260929/20121445BSTMzB1KTh.png

CDP 跑測試
    ↓
生成 md 測試結果(含 XPath)
    ↓
AI 讀 md,生成 pytest 腳本
    ↓
自動執行腳本
    ↓
失敗 → 自我修復 → 更新回腳本(最多 3 次)

注意那段 XPath 策略。

這是我覺得整個實驗裡最有價值的設計

我沒有讓 AI 隨便挑定位器,而是給了它一個優先序:

(這實際建議參考前端團隊設計的思路為主)

優先序 策略 為什麼
1 有明確 id 就用 id 最穩定,前端改版最不容易動到
2 完整文字組合 + 相對路徑 語意明確,看得懂在測什麼
3 完整 XPath 絕對路徑 最後手段,改版必死

再執行結果中 XPath 策略有確實照著我給的優先序跑,不是隨便抓一條絕對路徑了事。

這個原理就跟之前這篇 Browser-use 底層原理有很大的關係了,也是借鏡了這原理進行這的設計

  • Seletor、自我修復策略、執行紀錄細節 等等

🧨 踩坑:步驟一多,腳本準確度就崩了?

當下我以為:既然簡單的案例能生成,複雜的應該也只是多幾行而已。

實際發生的:完全不是這樣。

我一樣拿 Day14 兩個測試案例做對比:

案例 A(2 步):
https://ithelp.ithome.com.tw/upload/images/20260929/20121445R9vmG2ecwo.png

  • ✅ 腳本完全成功,一次就過。

案例 B:
https://ithelp.ithome.com.tw/upload/images/20260929/20121445VhajeOtGwm.png

  • MCP UI 操作過程沒什麼問題!⚠️ 但生成腳本的出錯率就高很多了⚠️。

MCP 執行 UI 的能力,跟「把執行過程翻譯成可重複的程式碼」的能力,是兩件事。

前者它做得很好,後者步驟一多就容易開始失真。

👉(這就像有人可以很流暢地開車載你到目的地,但你叫他把路線寫成一份別人看得懂的行車指南,可能就會寫得很零散。)

所以,CDP 缺的到底是什麼?

它是一個很好的「操作層」,但完整的測試流程需要的還是「框架層」。

CDP 有沒有 測試流程需不需要
開瀏覽器、點按鈕、輸入 ✅ 有,而且很穩 ✅ 需要
拿到 Console / Network ✅ 有,這是它最強的地方 ✅ 需要
明確的成功/失敗回饋 ✅ 有 ✅ 需要
自動登入 / 環境準備 ❌ 沒有 ✅ 必要
測試資料的建立與還原 ❌ 沒有 ✅ 必要
批次執行、排程 ❌ 沒有 ✅ 必要
結果收斂成報告 ⚠️ 有輸出,但格式要自己定 ✅ 需要
失敗了要通知誰 ❌ 沒有 ✅ 需要
這次學到的東西存下來 ❌ 完全沒有 ✅ 這才是關鍵

總結

  • 讓 CDP 順便生成 pytest 腳本是可行的,XPath 優先序策略要自己給,它會照做
  • 但步驟一多,腳本準確度就崩,UI 執行能力 ≠ 翻譯成可重複程式碼的能力
  • 生成的腳本現階段只能當參考,價值在於「省掉查元素那段」,不是直接能用
  • CDP 是很棒的操作層,但測試流程要的是框架層

pytest 腳本 Demo

附上案例二的腳本:

"""
Test Case: 購物車移除商品後結帳
來源: md_template/testcase.md
Xpath 由 Chrome DevTools MCP 實際執行時擷取的 DOM 屬性產生,
優先序: DOM id > 文字+相對路徑(data-test) > 絕對路徑
"""
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC

BASE_URL = "https://www.saucedemo.com"


@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--window-size=1280,900")
    drv = webdriver.Chrome(options=options)
    drv.implicitly_wait(5)
    yield drv
    drv.quit()


def test_cart_remove_item_then_checkout(driver):
    wait = WebDriverWait(driver, 10)

    # Step 1: 前往網站
    driver.get(BASE_URL)

    # Step 2: 登入
    wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="user-name"]'))).send_keys("standard_user")
    driver.find_element(By.XPATH, '//*[@id="password"]').send_keys("secret_sauce")
    driver.find_element(By.XPATH, '//*[@id="login-button"]').click()

    # 用實際畫面元素(排序下拉選單)出現作為登入完成的判斷依據,
    # 比只等 URL 變化更可靠:SPA 可能先更新網址列,畫面才渲染完成
    wait.until(EC.presence_of_element_located(
        (By.XPATH, '//select[@data-test="product-sort-container"]')
    ))

    # Step 3: 排序 Price (low to high)
    sort_select = Select(wait.until(
        EC.presence_of_element_located((By.XPATH, '//select[@data-test="product-sort-container"]'))
    ))
    sort_select.select_by_value("lohi")

    # Expect Result 1: 排序後第一個商品為 Sauce Labs Onesie($7.99)
    first_item_name = wait.until(
        EC.presence_of_element_located((By.XPATH, '(//div[@data-test="inventory-item-name"])[1]'))
    ).text
    first_item_price = driver.find_element(By.XPATH, '(//div[@data-test="inventory-item-price"])[1]').text
    assert first_item_name == "Sauce Labs Onesie", f"排序後第一項應為 Sauce Labs Onesie,實際為 {first_item_name}"
    assert first_item_price == "$7.99", f"排序後第一項價格應為 $7.99,實際為 {first_item_price}"

    # Step 4: 加入三項商品到購物車(含重試,因實際執行時觀察到偶發點擊未生效)
    def add_to_cart_with_retry(xpath, retries=3):
        for attempt in range(retries):
            driver.find_element(By.XPATH, xpath).click()
            try:
                wait.until(EC.presence_of_element_located(
                    (By.XPATH, xpath.replace('add-to-cart-', 'remove-'))
                ))
                return
            except Exception:
                if attempt == retries - 1:
                    raise
        raise AssertionError(f"加入購物車失敗,已重試 {retries} 次: {xpath}")

    add_to_cart_with_retry('//*[@id="add-to-cart-sauce-labs-onesie"]')
    add_to_cart_with_retry('//*[@id="add-to-cart-sauce-labs-bolt-t-shirt"]')
    add_to_cart_with_retry('//*[@id="add-to-cart-sauce-labs-backpack"]')

    cart_badge = driver.find_element(By.XPATH, '//*[@data-test="shopping-cart-badge"]').text
    assert cart_badge == "3", f"購物車應顯示 3 件,實際為 {cart_badge}"

    # Step 5: 進入購物車頁
    driver.find_element(By.XPATH, '//a[@data-test="shopping-cart-link"]').click()
    wait.until(EC.url_contains("cart.html"))

    # Step 6: 移除 Sauce Labs Bolt T-Shirt
    driver.find_element(By.XPATH, '//*[@id="remove-sauce-labs-bolt-t-shirt"]').click()
    wait.until(EC.invisibility_of_element_located((By.XPATH, '//*[@id="remove-sauce-labs-bolt-t-shirt"]')))

    # Step 7: Checkout,填資料,Continue
    driver.find_element(By.XPATH, '//*[@id="checkout"]').click()
    wait.until(EC.url_contains("checkout-step-one.html"))

    driver.find_element(By.XPATH, '//*[@id="first-name"]').send_keys("QA")
    driver.find_element(By.XPATH, '//*[@id="last-name"]').send_keys("Tester")
    driver.find_element(By.XPATH, '//*[@id="postal-code"]').send_keys("100")
    driver.find_element(By.XPATH, '//*[@id="continue"]').click()

    wait.until(EC.url_contains("checkout-step-two.html"))

    # Expect Result 2: 結帳確認頁只有 Onesie、Backpack 兩項
    item_names = [el.text for el in driver.find_elements(By.XPATH, '//div[@data-test="inventory-item-name"]')]
    assert set(item_names) == {"Sauce Labs Onesie", "Sauce Labs Backpack"}, f"結帳項目不符,實際為 {item_names}"
    assert len(item_names) == 2, f"結帳項目應為 2 項,實際為 {len(item_names)} 項"

    # Expect Result 3: 金額
    item_total = driver.find_element(By.XPATH, '//div[@class="summary_subtotal_label"]').text
    tax = driver.find_element(By.XPATH, '//div[@class="summary_tax_label"]').text
    total = driver.find_element(By.XPATH, '//div[@class="summary_total_label"]').text
    assert item_total == "Item total: $37.98", f"Item total 不符: {item_total}"
    assert tax == "Tax: $3.04", f"Tax 不符: {tax}"
    assert total == "Total: $41.02", f"Total 不符: {total}"

    # Step 8: Finish
    driver.find_element(By.XPATH, '//*[@id="finish"]').click()
    wait.until(EC.url_contains("checkout-complete.html"))

    # Expect Result 4: 出現 Thank you for your order!
    complete_header = driver.find_element(By.XPATH, '//h2[@data-test="complete-header"]').text
    assert complete_header == "Thank you for your order!", f"完成頁文案不符: {complete_header}"

    # Expect Result 5: 購物車圖示不再顯示數字
    badges = driver.find_elements(By.XPATH, '//*[@data-test="shopping-cart-badge"]')
    assert len(badges) == 0, "購物車圖示應不再顯示數字(無 badge 元素)"

參考來源


上一篇
[Day15] AI E2E - Chrome DevTools MCP 安裝與介紹:讓 AI 看得到細節才算真的有在測?
下一篇
[Day17] Browser-use vs Chrome DevTools MCP 比較:AI E2E 測試工具該怎麼選?
系列文
測試的前提正在改變:AI 時代下 QA 的思維轉換 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言