動Web自動化測試:自動生成Selenium與Playwright腳本)
每次產(chǎn)品提一嘴需求UI 改幾個樣式測試同學(xué)就要陪著熬夜改腳本——干過 Web 自動化測試的人應(yīng)該都懂這種痛。過去大半年我一直在折騰一套內(nèi)部代號叫 Mirage Flow 的工具核心思路一句話把頁面改寫成一段可讀的 UI 描述它自動生成對應(yīng)的 Selenium 或 Playwright 自動化測試腳本。聽起來像是“測試代碼不用手寫了”實操下來大部分常見頁面確實可以直接出能跑通的代碼。這篇我會把 Mirage Flow 從設(shè)計思路、描述格式、映射規(guī)則到雙框架生成代碼的實現(xiàn)細(xì)節(jié)完整拆開講也會把我在內(nèi)部分享時幫別人避開的坑一并列出來。適合正在搭自動化測試體系、被腳本維護逼到頭大的測試開發(fā)也適合想了解 UI 描述驅(qū)動測試代碼生成這個思路的前端和 QA 同學(xué)。1. 自動化測試腳本維護的痛和 UI 描述這個解法1.1 腳本為什么越寫越脆弱剛接觸自動化測試那會兒我也和大多數(shù)人一樣拿到一個頁面就按 F12 復(fù)制 XPath拼出一段“能跑就行”的腳本。等用例攢到幾百條問題就全冒出來了。最頭疼的是定位器太脆弱。開發(fā)把按鈕的 CSS 類名從btn-login改成btn-primary腳本直接紅燈某個輸入框的 placeholder 文案從“請輸入手機號”變成“請輸入手機號碼”整條用例就崩。類似的問題還有頁面加了異步加載腳本還在等固定 sleep接口超時報錯測試用例跟著誤報開發(fā)重構(gòu)了 DOM 層級一層層寫死的 XPath 全部失效。我曾經(jīng)統(tǒng)計過一個小版本迭代里腳本維護時間能占到總工作量的 60% 以上真正寫新功能的時間反而沒多少。更深一層的問題是協(xié)作成本。業(yè)務(wù)同事、新來的同學(xué)想看懂自動化腳本發(fā)現(xiàn)全是driver.find_element(By.XPATH, //div[3]/div[1]/div[2]/button)這種天書。他們提一句“登錄按鈕現(xiàn)在換個位置了”你都得在全量用例里搜一遍有多少地方寫死了這個按鈕的路徑。時間一長自動化測試變成了少數(shù)人的私產(chǎn)別人既不敢動也不敢提。這就有點像裝修時把所有開關(guān)位置都記在腦子里地圖和圖紙都不畫——家里一旦調(diào)整家具布局你只能摸黑挨個試。UI 描述驅(qū)動生成腳本的思路本質(zhì)上就是先畫一張“平面圖”讓每個控件有自己穩(wěn)定的身份再讓施工隊按圖去干活。頁面改了改圖比扒墻皮快得多。1.2 為什么要把“頁面描述”和“測試代碼”拆開Mirage Flow 最早想解決的問題其實很簡單能不能讓測試代碼不直接依賴某一條具體的 XPath而是依賴“這里有一個密碼輸入框”這類語義信息因為產(chǎn)品、UI、測試在評審時討論的從來不是#id_023而是“登錄頁有個密碼框”。于是我們把人要管的層次和機器要管的層次分開。測試人員維護一份 UI 描述描述里寫清楚頁面有什么元素、元素叫什么、有哪些動作和斷言生成器拿到描述之后再根據(jù)目標(biāo)框架的能力去選擇定位策略、生成等待代碼、拼裝腳本骨架。也就是說描述只回答“這個頁面長什么樣、我要做什么”代碼負(fù)責(zé)回答“具體怎么定位、怎么等待、怎么斷言”。這個拆分帶來的好處在很多真實場景里都被驗證過頁面改版時我們通常只要更新描述文件里的字段重新生成腳本不用把所有用例翻一遍新增一個同類頁面時復(fù)制一份描述改幾個字段整個用例就有了業(yè)務(wù)同學(xué)參與評審描述文件比讓他們看WebDriverWait代碼接地氣得多同一個描述可以同時生成 Selenium 和 Playwright 兩套代碼切換技術(shù)棧時不必推倒重來。當(dāng)然這套方案也不是萬能的。頁面結(jié)構(gòu)極其復(fù)雜、控件完全沒有穩(wěn)定屬性、交互邏輯非常特殊的時候純靠描述生成反而比手寫更繞。所以 Mirage Flow 的定位從來不是“取代手寫測試代碼”而是把那些重復(fù)的、模式化的 Web 操作從手寫變成生成讓人把精力留在真正需要判斷的地方。2. Mirage Flow 的核心設(shè)計思路與方案選型2.1 整體架構(gòu)到底分了哪幾層Mirage Flow 整體拆成四層這也是我后來給團隊做分享時最常用的分層方式。第一層是 UI 描述層。我們選用 YAML 作為描述文件的載體原因后面細(xì)說。這層只關(guān)心“頁面有哪些元素、元素類型是什么、有哪些動作和斷言”完全不涉及任何定位器和框架 API。第二層是解析與校驗層。生成器會讀取描述文件先做 Schema 校驗字段缺失、類型錯誤、引用不存在的元素都在這一層提前暴露。校驗通過后把 YAML 歸一化成內(nèi)部統(tǒng)一的 Python 字典結(jié)構(gòu)后續(xù)模板只要消費這一份結(jié)構(gòu)即可。第三層是代碼生成層。這里用了 Jinja2 模板引擎加一組針對元素類型的映射策略。生成器根據(jù)目標(biāo)框架Selenium 或 Playwright選擇不同的模板集模板里只負(fù)責(zé)拼裝代碼結(jié)構(gòu)具體的定位器計算和等待策略由策略模塊提供。第四層是執(zhí)行與反饋層。生成出來的腳本可以直接交給 pytest 執(zhí)行失敗時會截圖、抓取頁面源碼、輸出日志并和 Allure 報告做集成。執(zhí)行反饋又反過來告訴我們哪些元素描述偏向不穩(wěn)定可以在下次生成時主動提示。為什么要把這四層拆得這么干凈因為在實際迭代中四層的變化頻率完全不同。描述格式相對穩(wěn)定映射策略會隨項目演進不斷補充模板要跟隨兩個框架的 API 版本更新執(zhí)行層則要根據(jù)團隊基礎(chǔ)設(shè)施調(diào)整。如果把這四層揉在一個腳本里任何一層小改動都會引發(fā)連鎖問題后來者根本不敢動。2.2 為什么同時支持 Selenium 和 Playwright而不是二選一立項討論時團隊內(nèi)部也吵過一輪Selenium 生態(tài)成熟、團隊多少都會一點Playwright 自動等待和調(diào)試體驗好、是趨勢。最后結(jié)論是都要支持但理由不是站隊而是現(xiàn)實需求。我先用一張表把兩個框架在我們項目里的對比列出來維度SeleniumPlaywright生態(tài)成熟度高歷史資料和社區(qū)案例多相對較新團隊學(xué)習(xí)成本稍高自動等待需要手動寫顯式等待或 Polling內(nèi)置 Actionability 檢查默認(rèn)行為友好多 Tab / 多窗口需要切換窗口句柄代碼略繁瑣提供多頁面上下文API 更直觀iframe / Shadow DOMiframe 要顯式 switchShadow DOM 要額外處理frame_locator 與 Shadow DOM 支持更順滑網(wǎng)絡(luò)攔截與 Mock需要額外工具原生支持 route 攔截遺留系統(tǒng)兼容對老瀏覽器和復(fù)雜環(huán)境適應(yīng)性更好對現(xiàn)代 Chromium/Firefox/WebKit 更友好存量腳本遷移成本低很多團隊已有大量 Selenium 用例高我們真實的業(yè)務(wù)里存量項目一大半是 Selenium 腳本這些腳本背后還有一堆自定義工具函數(shù)、測試數(shù)據(jù)準(zhǔn)備邏輯、內(nèi)部框架封裝。如果要求全部遷移到 Playwright光遷移和驗證成本就夠嗆。而新立項的項目我們希望直接用 Playwright享受自動等待和多瀏覽器支持。所以 Mirage Flow 的設(shè)計原則是描述層保持中立生成層通過適配器輸出兩種代碼。對使用的人來說同一個描述文件既能生成舊項目的 Selenium 回歸腳本也能生成新項目的 Playwright 用例。你不需要想清楚“未來用什么框架”先把頁面描述沉淀下來框架遷移只是一個重新生成的動作。2.3 從 UI 描述到定位器的映射策略描述層里寫的是語義信息生成代碼時必須翻譯成真正的定位器。這一塊是 Mirage Flow 最核心的難點也是最需要反復(fù)打磨的部分。我們給元素定位設(shè)計了一個優(yōu)先級策略id優(yōu)先其次是>page: 登錄頁 url: /login elements: - name: 用戶名輸入框 type: input placeholder: 請輸入用戶名 - name: 密碼輸入框 type: input placeholder: 請輸入密碼 secure: true - name: 登錄按鈕 type: button text: 登錄 actions: - name: 輸入用戶名 element: 用戶名輸入框 action: fill data: ${username} - name: 輸入密碼 element: 密碼輸入框 action: fill data: ${password} - name: 點擊登錄 element: 登錄按鈕 action: click assertions: - name: 校驗登錄成功 element: 歡迎信息 type: visible這個文件的優(yōu)點在于沒有一行代碼但任何懂業(yè)務(wù)的人都能看懂我們在測什么。elements定義頁面元素actions定義操作序列assertions定義結(jié)果校驗。描述文件里最需要注意的是命名一致性。你在elements里給元素起的name是全局唯一標(biāo)識后面的actions和assertions都通過name引用它。我見過不少同事把同一個輸入框在元素定義里叫“手機號輸入框”在動作里寫成“輸入手機號”結(jié)果生成器匹配不上報錯信息還提示得不夠直接。后來我們在 Schema 校驗里強行要求引用任何元素前必須先保證該元素確實存在于elements列表否則解析直接失敗。3.2 元素類型識別與定位器生成的核心實現(xiàn)比較傻的做法是在模板里寫滿if type button這種硬編碼但一旦控件類型變多模板會變成一個巨大的分支地獄。Mirage Flow 里我們抽了一個locator_strategy.py每種元素類型對應(yīng)一個策略類由策略類負(fù)責(zé)計算定位表達(dá)式。以生成 CSS 選擇器為例核心邏輯大約是這樣def build_css_locator(element: dict) - str: # 優(yōu)先級id >from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver webdriver.Chrome() driver.maximize_window() try: driver.get(http://localhost:8080/login) username_input WebDriverWait(driver, 10).until( EC.presence_of_element_located( (By.CSS_SELECTOR, [placeholder*請輸入用戶名]) ) ) username_input.send_keys(test01) password_input WebDriverWait(driver, 10).until( EC.presence_of_element_located( (By.CSS_SELECTOR, [placeholder*請輸入密碼]) ) ) password_input.send_keys(abc123) login_button WebDriverWait(driver, 10).until( EC.element_to_be_clickable( (By.XPATH, //button[contains(text(),登錄)]) ) ) login_button.click() WebDriverWait(driver, 10).until( EC.visibility_of_element_located( (By.XPATH, //*[text()歡迎回來]) ) ) finally: driver.quit()生成代碼時我特別要求把WebDriverWait的顯式等待寫進去而不是讓框架替我們偷懶用time.sleep。原因很簡單Selenium 沒有內(nèi)建 Actionability 檢查presence_of_element_located只代表元素出現(xiàn)了不代表能被點擊。如果生成器偷懶生成的腳本八成在真實環(huán)境里跑不穩(wěn)。所以對可點擊元素我們一律生成element_to_be_clickable對需要輸入的控件至少生成presence_of_element_located保證元素被渲染出來。生成器還處理了一些“看起來不起眼但很要命”的細(xì)節(jié)finally里必須driver.quit()防止并發(fā)跑用例時進程不釋放關(guān)鍵操作后如果描述里開啟了截圖自動插入截圖代碼最后把異常信息帶出方便 pytest 捕獲。3.4 生成 Playwright 代碼時差異點比想象中大剛開始我以為 Playwright 代碼就是把find_element換成locator而已實際上手才發(fā)現(xiàn)兩個框架的生成邏輯差異遠(yuǎn)比 API 名稱差異大。Playwright 最大的不同是自帶 Actionability 檢查click()方法會自動等待元素可見、穩(wěn)定、可點擊。這帶來一個直接后果生成代碼里不需要堆WebDriverWait。但我們必須理解它的等待語義否則生成的代碼可能用錯等待方式。一個典型的 Playwright 生成結(jié)果from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch() page browser.new_page() page.goto(http://localhost:8080/login) page.fill([placeholder*請輸入用戶名], test01) page.fill([placeholder*請輸入密碼], abc123) page.click(button:has-text(登錄)) page.wait_for_selector(text歡迎回來) browser.close()兩套代碼放在一起看差異就非常直觀Selenium 版本需要手動等待、手動選擇顯式條件生成器要替用戶做很多決策Playwright 版本自帶等待生成器只需要保證定位器合理代碼看起來簡潔得多Playwright 的多 Tab 和 iframe 處理方式不同生成器生成的代碼結(jié)構(gòu)也不同。我們曾經(jīng)給一個涉及內(nèi)部系統(tǒng) iframe 的頁面生成 Playwright 代碼生成器直接輸出frame_locator(iframe[titlemain]).locator(...)這比 Selenium 里switch_to.frame再定位的代碼可讀性好太多。雙框架生成讓我學(xué)到一個教訓(xùn)不要試圖在一套抽象里掩蓋框架的差異因為這樣生成的代碼必然是你妥協(xié)后的“最小公分母”。Mirage Flow 的做法是讓描述層完全相同但生成模板按框架能力各自發(fā)揮。Playwright 模板可以用它的自動等待優(yōu)勢Selenium 模板就老老實實生成顯式等待兩者生成的代碼都符合各自社區(qū)的最佳實踐。3.5 數(shù)據(jù)驅(qū)動和斷言怎么生成到腳本里自動化測試不可能只有一組數(shù)據(jù)。登錄頁至少要測正確密碼和錯誤密碼搜索結(jié)果頁要測不同關(guān)鍵詞。所以描述文件里的${username}、${password}這類變量必須能透傳給測試框架。我們最終選擇讓生成器輸出 pytest 參數(shù)化入口import pytest from yourgen_module.login_page import run_login pytest.mark.parametrize(username,password,expect_success, [ (test01, abc123, True), (test02, wrong, False), ]) def test_login(username, password, expect_success): run_login(usernameusername, passwordpassword, expect_successexpect_success)這樣設(shè)計的好處是數(shù)據(jù)和業(yè)務(wù)邏輯完全分離。測試數(shù)據(jù)可以放在 JSON/Excel 里再由一個很小的加載邏輯喂給參數(shù)化裝飾器業(yè)務(wù)腳本則由生成器維護描述文件更新后重新生成即可。斷言部分也是一樣。描述文件里寫了assertions生成器會翻譯成對應(yīng)框架的斷言代碼。我們支持了最常見的幾類元素可見、元素文本等于、元素數(shù)量等于、元素屬性包含、當(dāng)前 URL 包含。每類斷言在不同框架里寫法不同所以這塊也是雙模板的重點。不要小看斷言描述的重要性它其實是把測試意圖沉淀下來比只記錄操作路徑更有價值。4. 常見問題與排查技巧實錄4.1 定位器生成出來卻還是不穩(wěn)定問題出在哪Mirage Flow 上線第一個月被問得最多的就是“為什么我根據(jù)描述生成了選擇器跑兩次還是掛了”。我排查了一圈發(fā)現(xiàn)絕大多數(shù)情況不是生成器的問題而是頁面里存在多個滿足條件的節(jié)點。舉例來說頁面里有三個按鈕文本都包含“提交”描述里寫type: button, text: 提交生成器用:has-text(提交)或contains(text(),提交)定位時匹配到多個節(jié)點。Selenium 會直接拋ElementNotInteractableException或StaleElementReferenceExceptionPlaywright 的strict mode會直接報錯提示“l(fā)ocator resolved to 3 elements”。這個我后來在描述規(guī)范里加了一條規(guī)則如果元素文本模糊請務(wù)必補充位置信息比如index: 0或者寫一個更精確的父級描述。另一個常見坑是頁面里存在不可見元素。比如開發(fā)用隱藏的input做狀態(tài)存儲placeholder 恰好又和真實輸入框類似。這時候顯式等待能查到元素但send_keys卻報錯。我們在生成 Selenium 代碼時特意對fill動作增加visibility_of_element_located而不是presence_of_element_located能減少一部分誤報。排查這類問題我推薦一個土辦法把生成的 locator 直接粘到瀏覽器 DevTools 的 Console 里執(zhí)行document.querySelectorAll(...)數(shù)數(shù)匹配了幾個節(jié)點。凡是能立刻看出匹配數(shù)量不對基本都能在生成階段發(fā)現(xiàn)問題不用反復(fù)跑用例試錯。4.2 iframe 和 Shadow DOM描述層怎么表達(dá)現(xiàn)在的系統(tǒng)里嵌套 iframe、Shadow DOM 已經(jīng)很常見。早期描述文件里沒有這個概念生成的腳本遇到內(nèi)部系統(tǒng)頁面就必掛。后來我們在元素描述里增加了兩個可選字段- name: 內(nèi)部操作按鈕 type: button text: 確定 frame: iframe[titlemain] shadow: true生成器遇到frame字段時Selenium 側(cè)會生成切 frame 再定位的代碼Playwright 側(cè)則生成frame_locator(...).locator(...)。遇到shadow字段時處理方式又不一樣Playwright 默認(rèn)可以穿透 open shadow root而 Selenium 需要先取shadowRoot再往里查。這些差異統(tǒng)統(tǒng)被封裝在模板層對描述作者來說只是多寫兩個字段。這里有個值得強調(diào)的點如果不確定元素到底在不在 iframe 里可以先跑一次帶--debug的生成模式工具會輸出候選定位器在頁面上的命中情況。比起在腳本里加斷點這個階段排查成本低得多。4.3 等待策略固定 sleep 是最毒的用法我見過太多現(xiàn)有自動化項目里全是time.sleep(5)。剛開始腳本能跑等頁面變慢或者接口變慢sleep 不夠就掛為了解決掛有人把 sleep 加到 10 秒、15 秒于是整個用例集跑到要 40 分鐘。這種代碼不能算自動化測試只能叫定時器表演。Mirage Flow 生成的代碼里我們默認(rèn)不輸出任何固定 sleep。Selenium 側(cè)用WebDriverWait配合具體條件Playwright 側(cè)直接依賴自動等待能力。唯一允許出現(xiàn)page.wait_for_timeout(500)的場景是那些頁面里有動畫過渡、且動畫結(jié)束后元素才可交互的罕見情況而且我們會在描述里顯式標(biāo)注delay: 500讓使用者意識到這里有一個不經(jīng)意的等待。如果用例跑下來還是不穩(wěn)定第一步永遠(yuǎn)不是加 sleep而是打開瀏覽器的 Network 面板看請求到底卡在哪。是后端 5 秒才返回還是前端 JS 渲染慢了定位到原因后或者調(diào)整接口 Mock或者改用更合理的等待條件。把“等待“這件事交給廣播式輪詢而不是固定假設(shè)頁面一定在 N 秒內(nèi)加載完穩(wěn)定性會完全不一樣。4.4 和 CI 集成時最容易踩的三個坑生成出來的腳本最終要跑在流水線里和本地跑完全是兩種體驗。最先踩的坑是測試環(huán)境地址寫死在描述文件里。我們在描述文件里用${base_url}這種變量占位CI 里通過環(huán)境變量注入這樣同一套描述可以隨便切換 dev / staging / prod 環(huán)境。第二個坑是失敗證據(jù)收集。CI 里腳本掛了人不在現(xiàn)場連截圖都看不到。我們在 pytest 的conftest.py里加了 fixture每個用例失敗時自動截圖并附加到 Allure 報告同時抓取當(dāng)前頁面 URL 和頁面源碼。這一步對排查定位器失效特別有用因為很多時候光看報錯信息根本不知道頁面當(dāng)時長什么樣。第三個坑是瀏覽器并發(fā)和資源占用。早期我們在 CI 里同時起十個 Chrome 進程跑不同用例經(jīng)常把節(jié)點內(nèi)存打滿出現(xiàn)一堆莫名其妙的白屏和超時。后來做了兩件事一是控制并行度二是給每個任務(wù)固定臨時目錄避免多個瀏覽器搶同一份用戶數(shù)據(jù)目錄。看起來很基礎(chǔ)但確實救了我們很多次。5. 一些實際體會和擴展方向Mirage Flow 做到現(xiàn)在最有價值的不是它生成了多少行代碼而是逼著我們把“頁面怎么測”這件事用結(jié)構(gòu)化方式沉淀下來。以前討論一條用例是否合理大家得先翻代碼現(xiàn)在直接看描述文件一條條核對元素和斷言效率高很多。我個人的經(jīng)驗是先覆蓋高頻控件不要妄想一步到位。按鈕、輸入框、下拉、單選、文本斷言這幾類能覆蓋日常 80% 的頁面交互。表格、彈窗、拖拽、復(fù)雜動畫這類特殊場景寧可讓它落地在描述里留手工擴展口也不要讓模板無限變復(fù)雜。模板越花哨后期維護越痛苦。后續(xù)我打算做兩件事。一是把描述文件做得更“寬松”允許自然語言描述加參數(shù)配置混合使用比如“點擊搜索按鈕”結(jié)合type: button一起解析讓非測試同學(xué)也能寫描述。二是把定位器穩(wěn)定性統(tǒng)計接回來每次執(zhí)行后記錄哪些 locator 匹配多個節(jié)點、哪些頁面元素經(jīng)常變化讓生成器根據(jù)歷史執(zhí)行數(shù)據(jù)調(diào)整策略優(yōu)先級。這些都是基于現(xiàn)有描述層之上的增量不用推翻架構(gòu)這也是當(dāng)初分層設(shè)計最值得的一筆投資。如果讓我重新來一遍我會在立項時就把描述文件的 Schema 定得更嚴(yán)一些字段窮舉清楚而不是邊用邊加?,F(xiàn)在雖然做了向后兼容處理但早期那些“摸著石頭過河”留下的模糊字段依然會在自動生成時造成語義歧義。自動化工具最怕的不是功能少而是規(guī)則不明確。把規(guī)則寫清楚把例外留給人判斷這套思路放到任何自動化項目里都適用。