試實(shí)戰(zhàn):從環(huán)境搭建到CI流水線的完整指南)
1. 端到端測(cè)試到底在測(cè)什么先說(shuō)結(jié)論e2eend-to-end這個(gè)縮寫(xiě)在我這個(gè)崗位每天都會(huì)出現(xiàn)它不是某個(gè)工具的名字而是一整套測(cè)試思路——把整個(gè)系統(tǒng)當(dāng)成一個(gè)黑盒從用戶視角走完一條完整的業(yè)務(wù)鏈路驗(yàn)證用戶能做的事情系統(tǒng)真的能讓他做成。舉個(gè)最直白的例子。你做一個(gè)電商網(wǎng)站單元測(cè)試只驗(yàn)證購(gòu)物車(chē)加一件商品后數(shù)量從0變成1接口測(cè)試只驗(yàn)證POST /cart 返回200。但用戶不會(huì)關(guān)心這些用戶關(guān)心的是打開(kāi)首頁(yè)、搜商品、點(diǎn)加入購(gòu)物車(chē)、去結(jié)算、填地址、付款——這一串動(dòng)作能不能流暢走通。這個(gè)過(guò)程里任何一個(gè)環(huán)節(jié)出問(wèn)題哪怕中間十個(gè)接口都返回正常用戶層面最終就是買(mǎi)不了東西。e2e測(cè)的就是這條完整鏈路。我剛工作那兩年也不太理解為什么要單獨(dú)搞e2e總覺(jué)得單元測(cè)試和接口測(cè)試覆蓋率夠了就萬(wàn)事大吉。后來(lái)線上出過(guò)一次事故下單接口返回成功但前端跳轉(zhuǎn)支付頁(yè)的時(shí)候因?yàn)橐粋€(gè)字段名對(duì)不上頁(yè)面白屏。接口測(cè)試和單測(cè)全綠用戶就是付不了錢(qián)。那次之后我徹底想明白了測(cè)試金字塔再漂亮缺了e2e這層你永遠(yuǎn)不知道拼起來(lái)之后系統(tǒng)到底能不能用。這篇文章我用實(shí)際項(xiàng)目里的經(jīng)驗(yàn)把e2e從環(huán)境搭建、用例設(shè)計(jì)、執(zhí)行策略到問(wèn)題排查完整拆一遍。適合剛接觸e2e的測(cè)試開(kāi)發(fā)也適合寫(xiě)了幾年e2e但總覺(jué)得用例跑不穩(wěn)的同學(xué)對(duì)照參考。2. 環(huán)境準(zhǔn)備工具選型和profile1配置的來(lái)龍去脈2.1 測(cè)試框架選型別盲目追新e2e的工具生態(tài)這幾年變化很快。老牌的Selenium還在但它走的是WebDriver協(xié)議啟動(dòng)瀏覽器、執(zhí)行命令的鏈路很長(zhǎng)跑起來(lái)慢而且對(duì)現(xiàn)代前端框架里那種動(dòng)態(tài)DOM的兼容性越來(lái)越吃力。Cypress當(dāng)年很火把執(zhí)行環(huán)境從瀏覽器外面挪到瀏覽器內(nèi)部速度確實(shí)快了一個(gè)量級(jí)但它有個(gè)天然的短板只能在Chromium系跑多瀏覽器覆蓋不方便而且它對(duì)多標(biāo)簽頁(yè)、跨域場(chǎng)景支持得很別扭。目前我主推Playwright。它是微軟開(kāi)源的項(xiàng)目底層走的是CDPChrome DevTools Protocol啟動(dòng)速度和執(zhí)行穩(wěn)定性比Selenium強(qiáng)太多API設(shè)計(jì)上對(duì)等待策略做了內(nèi)置處理能自動(dòng)等元素可見(jiàn)、可點(diǎn)、可操作一個(gè)腳本能同時(shí)控Chromium、Firefox、WebKit三種內(nèi)核對(duì)用戶可能用不同瀏覽器訪問(wèn)這個(gè)場(chǎng)景覆蓋得最省力。工具選型這件事我不建議糾結(jié)太久記住一個(gè)原則有長(zhǎng)期維護(hù)、社區(qū)活躍、API設(shè)計(jì)符合直覺(jué)、能覆蓋你的目標(biāo)瀏覽器。目前Playwright在這幾個(gè)維度上都是最優(yōu)解。2.2 e2e profile1是什么意思很多項(xiàng)目中你都會(huì)看到測(cè)試配置文件帶profile字樣的命名比如profile1其實(shí)就是一套端到端測(cè)試的崗位職責(zé)配置文件。不同項(xiàng)目叫法不太一樣有的叫env有的叫config有的叫profile。核心作用只有一個(gè)把跑哪套環(huán)境、連哪個(gè)服務(wù)、用哪組賬號(hào)、跑哪些用例這些決定測(cè)試行為的參數(shù)從代碼里抽離出來(lái)單獨(dú)管理。我習(xí)慣這樣組織profile1對(duì)應(yīng)默認(rèn)的本地開(kāi)發(fā)和測(cè)試環(huán)境localhost 測(cè)試庫(kù) 測(cè)試賬號(hào)組profile2對(duì)應(yīng)預(yù)發(fā)布環(huán)境staging 預(yù)發(fā)布庫(kù) 隔離賬號(hào)組profile3對(duì)應(yīng)生產(chǎn)驗(yàn)證環(huán)境灰度域名 只讀賬號(hào) 核心鏈路用例這樣做的原因很樸素一套測(cè)試腳本如果能同時(shí)適配多套環(huán)境那它就是可復(fù)用的資產(chǎn)而不是一次性腳本。你想想開(kāi)發(fā)環(huán)境、測(cè)試環(huán)境、預(yù)發(fā)布環(huán)境、生產(chǎn)環(huán)境本質(zhì)上是同一套業(yè)務(wù)代碼跑在不同配置下沒(méi)理由每次換環(huán)境就重寫(xiě)一遍測(cè)試。2.3 項(xiàng)目目錄結(jié)構(gòu)和基礎(chǔ)配置先看一個(gè)我常用的項(xiàng)目初始化結(jié)構(gòu)e2e-project/ ├── package.json ├── playwright.config.ts ├── profiles/ │ ├── profile1.json │ ├── profile2.json └── tests/ ├── auth.spec.ts ├── order.spec.ts └── payment.spec.tspackage.json里需要裝的東西很少核心就一個(gè)npm init -y npm install -D playwright/test npx playwright installnpx playwright install這條命令會(huì)把三個(gè)內(nèi)核的瀏覽器二進(jìn)制下載到本地這一步在國(guó)內(nèi)網(wǎng)絡(luò)環(huán)境下有時(shí)候比較慢建議提前配好鏡像源或者手動(dòng)下載瀏覽器包放到緩存目錄能省不少時(shí)間。然后是配置文件核心部分// playwright.config.ts import { defineConfig, devices } from playwright/test; import profile1 from ./profiles/profile1.json; export default defineConfig({ testDir: ./tests, timeout: 60000, retries: 1, use: { baseURL: profile1.baseURL, trace: retain-on-failure, video: on-first-retry, }, projects: [ { name: chromium, use: { ...devices[Desktop Chrome] }, }, ], });這里最關(guān)鍵的是讀取了profile1.json里的baseURL整個(gè)測(cè)試套件跑哪套環(huán)境完全由profile文件決定改測(cè)試代碼的頻次大大降低。3. 用例設(shè)計(jì)思路從業(yè)務(wù)鏈路反推測(cè)試步驟3.1 用戶旅程優(yōu)先e2e用例設(shè)計(jì)的最大誤區(qū)是照著接口文檔一個(gè)個(gè)點(diǎn)頁(yè)面。真正有效的做法是先從業(yè)務(wù)價(jià)值出發(fā)把用戶最常走的路徑列出來(lái)然后逐個(gè)覆蓋。以電商系統(tǒng)為例最高優(yōu)先級(jí)的用戶旅程有幾條登錄 - 搜索商品 - 加購(gòu)物車(chē) - 結(jié)算 - 下單成功登錄 - 領(lǐng)取優(yōu)惠券 - 結(jié)算時(shí)校驗(yàn)價(jià)格 - 支付 - 訂單狀態(tài)更新未登錄 - 瀏覽商品 - 加購(gòu)物車(chē) - 結(jié)算時(shí)引導(dǎo)登錄 - 登錄后訂單鏈路繼續(xù)這三條鏈路的共同點(diǎn)是每一步都依賴上一步成功任一處斷裂用戶行為就終止了。e2e的價(jià)值恰恰在于驗(yàn)證這種連續(xù)依賴。設(shè)計(jì)用例的時(shí)候我習(xí)慣先畫(huà)一條業(yè)務(wù)時(shí)序然后確認(rèn)每一步的頁(yè)面入口和斷言點(diǎn)。比如登錄后用戶名出現(xiàn)在頁(yè)面右上角是斷言點(diǎn)搜索結(jié)果的第一個(gè)商品標(biāo)題包含關(guān)鍵詞也是斷言點(diǎn)。斷言點(diǎn)不一定要多但必須要能證明這一步真的成功。3.2 用例數(shù)量控制不是越多越好很多人一上來(lái)就寫(xiě)幾百條e2e用例跑一遍一兩個(gè)小時(shí)結(jié)果每天光排查flaky就花掉半天。e2e用例的定位是核心鏈路守門(mén)員不是全功能覆蓋者。全功能覆蓋應(yīng)該由單元測(cè)試和接口測(cè)試去完成。我個(gè)人的經(jīng)驗(yàn)是一個(gè)中等復(fù)雜度的后端系統(tǒng)e2e用例控制在30到50條比較合理。每條用例對(duì)應(yīng)一條關(guān)鍵用戶旅程或者一個(gè)核心業(yè)務(wù)規(guī)則。如果用例數(shù)量超過(guò)100條先看看是不是把本該在接口層測(cè)的東西寫(xiě)到了e2e里。3.3 數(shù)據(jù)準(zhǔn)備與清理策略e2e測(cè)試最頭疼的問(wèn)題是數(shù)據(jù)。測(cè)試環(huán)境的數(shù)據(jù)總量、污染程度、并發(fā)隔離方式直接決定了用例的穩(wěn)定性。我的做法是優(yōu)先用測(cè)試框架的API造數(shù)據(jù)不要通過(guò)UI一步步創(chuàng)建。比如注冊(cè)用戶這個(gè)前置條件直接調(diào)后端的注冊(cè)接口生成賬號(hào)比用UI跑一遍注冊(cè)流程快得多也省去UI注冊(cè)時(shí)的驗(yàn)證碼、短信等干擾。用例之間隔離每個(gè)用例盡量使用獨(dú)立數(shù)據(jù)。比如下單用例每次生成一個(gè)新的隨機(jī)手機(jī)號(hào)作為登錄賬號(hào)避免上一個(gè)用例殘留的訂單數(shù)據(jù)影響下一個(gè)用例。測(cè)試數(shù)據(jù)清理跑完用例后把創(chuàng)建的數(shù)據(jù)在teardown里清理掉。如果清理成本高至少保證測(cè)試數(shù)據(jù)有統(tǒng)一前綴便于定期批量清理。4. 從零寫(xiě)一條完整的e2e用例4.1 登錄注冊(cè)模塊的e2e實(shí)戰(zhàn)我拿注冊(cè) 登錄這條鏈路來(lái)演示完整寫(xiě)法。注冊(cè)登錄是幾乎所有系統(tǒng)的入口也是e2e里最容易出現(xiàn)理解偏差的模塊——很多人把它當(dāng)兩個(gè)用例寫(xiě)其實(shí)從用戶視角來(lái)看注冊(cè)完之后自動(dòng)登錄這是一條連續(xù)旅程。import { test, expect } from playwright/test; test(新用戶注冊(cè)后自動(dòng)登錄并進(jìn)入首頁(yè), async ({ page }) { // 生成唯一手機(jī)號(hào)保證每次運(yùn)行不會(huì)撞庫(kù) const timestamp Date.now(); const phone 138${String(timestamp).slice(-8)}; const password Test12345; // 打開(kāi)注冊(cè)頁(yè)面 await page.goto(/register); // 填寫(xiě)注冊(cè)表單 await page.getByLabel(手機(jī)號(hào)).fill(phone); await page.getByLabel(設(shè)置密碼).fill(password); await page.getByLabel(確認(rèn)密碼).fill(password); // 提交注冊(cè) await page.getByRole(button, { name: 注冊(cè) }).click(); // 關(guān)鍵斷言注冊(cè)成功后應(yīng)自動(dòng)登錄并跳轉(zhuǎn)首頁(yè) await expect(page).toHaveURL(/\/home/); // 斷言登錄態(tài)右上角出現(xiàn)用戶昵稱(chēng)或手機(jī)號(hào)掩碼 const userMenu page.getByTestId(user-menu); await expect(userMenu).toBeVisible(); await expect(userMenu).toContainText(phone.slice(0, 3) ****); });這里有幾個(gè)細(xì)節(jié)值得展開(kāi)說(shuō)。手機(jī)號(hào)用時(shí)間戳生成是為了防止測(cè)試環(huán)境里出現(xiàn)重復(fù)賬號(hào)如果不用時(shí)間戳第二次跑同一個(gè)用例就會(huì)遇到手機(jī)號(hào)已注冊(cè)的異常用例直接失敗但這不是被測(cè)系統(tǒng)的bug是測(cè)試代碼自己的問(wèn)題。這類(lèi)問(wèn)題統(tǒng)稱(chēng)為測(cè)試自身不穩(wěn)定因素要在設(shè)計(jì)階段就規(guī)避掉。getByLabel、getByRole、getByTestId這些是Playwright推薦的定位API優(yōu)先于className和id選擇器。原因在于className和id是前端代碼的實(shí)現(xiàn)細(xì)節(jié)重構(gòu)時(shí)很容易變而label、role、testid這些相對(duì)更接近用戶可見(jiàn)的語(yǔ)義穩(wěn)定性要好很多。我見(jiàn)過(guò)太多項(xiàng)目因?yàn)榍岸税杨?lèi)名從btn-login改成了login-button結(jié)果整個(gè)e2e套件紅一片。用語(yǔ)義化定位器之后這種情況基本絕跡。4.2 電商下單全流程用例下單鏈路是e2e中最能體現(xiàn)端到端價(jià)值的場(chǎng)景。這里要覆蓋搜索、加購(gòu)、購(gòu)物車(chē)校驗(yàn)、結(jié)算、支付回調(diào)、訂單生成一整串流程。test(用戶購(gòu)買(mǎi)商品后訂單狀態(tài)為待支付, async ({ page }) { // 登錄這里直接調(diào)用接口造數(shù)據(jù)不走UI const phone 13800000001; await page.request.post(/api/auth/login, { data: { phone, password: Test12345 }, }); // 進(jìn)入首頁(yè)搜索商品 await page.goto(/); await page.getByPlaceholder(搜索商品).fill(無(wú)線耳機(jī)); await page.keyboard.press(Enter); // 第一個(gè)搜索結(jié)果點(diǎn)擊進(jìn)入詳情 const firstItem page.locator(.product-list-item).first(); await firstItem.click(); // 詳情頁(yè)加購(gòu) await page.getByRole(button, { name: 加入購(gòu)物車(chē) }).click(); // 去購(gòu)物車(chē)頁(yè)結(jié)算 await page.goto(/cart); await expect(page.getByText(無(wú)線耳機(jī))).toBeVisible(); await page.getByRole(button, { name: 去結(jié)算 }).click(); // 結(jié)算頁(yè)確認(rèn)訂單 await page.getByRole(button, { name: 提交訂單 }).click(); // 跳轉(zhuǎn)支付頁(yè)驗(yàn)證訂單號(hào)存在 await expect(page).toHaveURL(/\/payment/); const orderNo await page.locator(.order-no).innerText(); expect(orderNo.length).toBeGreaterThan(0); });這條用例我特意把登錄前置到api層理由很簡(jiǎn)單登錄本身不是這條用例的測(cè)試目標(biāo)下單才是。如果讓每條下單用例都先走一遍UI登錄不但拖慢執(zhí)行時(shí)間還會(huì)因?yàn)榈卿浤K的偶發(fā)問(wèn)題污染其他模塊的測(cè)試結(jié)果。e2e的粒度要遵循只測(cè)這條鏈路應(yīng)該測(cè)的東西這個(gè)原則。4.3 踩過(guò)的坑選擇器、等待和斷言時(shí)機(jī)寫(xiě)e2e必然會(huì)遇到三個(gè)經(jīng)典問(wèn)題。第一個(gè)是選擇器不穩(wěn)定。我剛才一直強(qiáng)調(diào)語(yǔ)義化定位器但這還不夠。很多現(xiàn)代前端框架比如Vue、React渲染是異步的數(shù)據(jù)加載中頁(yè)面先顯示骨架屏數(shù)據(jù)到了之后才渲染真實(shí)內(nèi)容。如果在骨架屏階段就去定位元素大概率會(huì)失敗。Playwright的自動(dòng)等待機(jī)制解決了一部分問(wèn)題但仍要養(yǎng)成習(xí)慣關(guān)鍵交互前先斷言前置條件比如等加載完成標(biāo)識(shí)出現(xiàn)再點(diǎn)擊按鈕。第二個(gè)是等待策略。不要用硬編碼的sleep比如page.waitForTimeout(3000)。這類(lèi)寫(xiě)法是flaky用例的最大源頭——這3秒在網(wǎng)絡(luò)快的時(shí)候是浪費(fèi)在網(wǎng)絡(luò)慢的時(shí)候還不夠。正確做法是等待某個(gè)期望狀態(tài)出現(xiàn)Playwright里就是await expect(locator).toBeVisible()這類(lèi)斷言式等待它會(huì)在元素滿足條件時(shí)立刻返回不滿足時(shí)一直重試直到超時(shí)。這套機(jī)制比固定等待穩(wěn)得多。第三個(gè)是斷言時(shí)機(jī)。很多新手在頁(yè)面跳轉(zhuǎn)后立刻斷言URL但跳轉(zhuǎn)過(guò)程可能還在路由切換中斷言太早就會(huì)失敗。正確做法是先斷言一個(gè)目標(biāo)頁(yè)面特有的元素再斷言URL。比如跳轉(zhuǎn)支付頁(yè)后先等支付金額這個(gè)元素可見(jiàn)再校驗(yàn)URL里有沒(méi)有payment字段順序別反過(guò)來(lái)。5. 執(zhí)行策略從本地調(diào)試到CI流水線5.1 本地運(yùn)行 Debug 模式不直接跑全量我在本地開(kāi)發(fā)時(shí)很少一上來(lái)就跑全量測(cè)試那樣效率極低。Playwright提供了headed模式和調(diào)試模式我會(huì)先用--headed跑一條單用例肉眼確認(rèn)頁(yè)面行為對(duì)得上預(yù)期。如果中途出問(wèn)題可以打開(kāi)trace viewer回放每一步操作和頁(yè)面截圖定位是選擇器寫(xiě)錯(cuò)還是頁(yè)面邏輯有變化。調(diào)試完之后再切回headless模式跑全量回歸。實(shí)際使用中trace這項(xiàng)功能特別值得開(kāi)。它會(huì)在測(cè)試失敗時(shí)自動(dòng)記錄一段執(zhí)行軌跡包含網(wǎng)絡(luò)請(qǐng)求、DOM快照、控制臺(tái)日志、鼠標(biāo)鍵盤(pán)事件。排查線上環(huán)境復(fù)現(xiàn)不了的問(wèn)題靠它基本能一次定位。5.2 CI里怎么跑e2e持續(xù)集成中跑e2e最怕兩件事環(huán)境不一致和并發(fā)沖突。我見(jiàn)過(guò)太多項(xiàng)目在本地跑得干干凈凈一上CI就各種失敗十有八九是環(huán)境差異導(dǎo)致的。我的做法是在CI里用容器來(lái)跑測(cè)試保證每次跑的瀏覽器版本、Node版本、依賴版本完全一致。GitHub Actions里可以這樣配置name: e2e-tests on: push: branches: [main] pull_request: jobs: e2e: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: root ports: [3306:3306] steps: - uses: actions/checkoutv3 - uses: actions/setup-nodev3 with: node-version: 18 - run: npm ci - run: npx playwright install --with-deps - run: npx playwright test這個(gè)配置里把數(shù)據(jù)庫(kù)也作為服務(wù)一起起在CI里測(cè)試環(huán)境就能完整自包含不需要依賴外部環(huán)境。并發(fā)執(zhí)行方面Playwright默認(rèn)是跑在多個(gè)worker里的可以同時(shí)跑多個(gè)用例文件。但要特別注意多個(gè)worker同時(shí)操作同一個(gè)測(cè)試環(huán)境如果環(huán)境有共享數(shù)據(jù)就會(huì)產(chǎn)生互相干擾。比如兩個(gè)用例同時(shí)下單庫(kù)存數(shù)可能被兩個(gè)用例同時(shí)扣減最后斷言的口徑就亂了。我的方案是盡量讓每個(gè)用例的數(shù)據(jù)互相獨(dú)立如果實(shí)在沒(méi)辦法獨(dú)立就在playwright.config里把用例串行執(zhí)行export default defineConfig({ workers: process.env.CI ? 2 : 1, fullyParallel: false, });5.3 報(bào)告和失敗通知測(cè)試跑完報(bào)告必須讓人一眼看出哪破了、破在哪。Playwright默認(rèn)生成HTML報(bào)告包含每個(gè)用例的耗時(shí)、狀態(tài)、失敗截圖、trace回放入口。我會(huì)在CI里把報(bào)告上傳為artifact每次構(gòu)建完都能直接查看。失敗通知我用的是定時(shí)任務(wù)方式CI跑完如果非零退出就把結(jié)果推到團(tuán)隊(duì)的IM群里。這一步的核心價(jià)值在于快速響應(yīng)e2e掛了往往意味著主干鏈路有問(wèn)題早一分鐘知道就少一批受影響的用戶。6. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄6.1 flaky測(cè)試的三大來(lái)源和解決思路flaky測(cè)試就是那種這次跑過(guò)、下次跑掛、重跑又過(guò)了的用例。它比穩(wěn)定的失敗更讓人頭疼因?yàn)槟悴淮_定是自己代碼的問(wèn)題、測(cè)試的問(wèn)題還是環(huán)境的問(wèn)題。歸納下來(lái)98%的flaky來(lái)自下面三個(gè)地方來(lái)源典型表現(xiàn)解決方向異步渲染競(jìng)態(tài)頁(yè)面還沒(méi)渲染完就去點(diǎn)按鈕用斷言式等待替代固定sleep數(shù)據(jù)污染上一個(gè)用例殘留數(shù)據(jù)影響下一個(gè)每個(gè)用例獨(dú)立數(shù)據(jù)teardown清理環(huán)境抖動(dòng)網(wǎng)絡(luò)慢、第三方接口超時(shí)增加重試機(jī)制關(guān)鍵接口mock重試機(jī)制我建議謹(jǐn)慎使用。在CI里配置retries: 1是合理的超過(guò)3次重試就有問(wèn)題了——它不是幫你解決問(wèn)題而是幫你掩蓋問(wèn)題。重試通過(guò)之后沒(méi)有人為這個(gè)flaky根因負(fù)責(zé)它就會(huì)一直潛伏下去。6.2 定位失敗用例的標(biāo)準(zhǔn)排查流程遇到用例失敗了我現(xiàn)在的排查流程基本固定分享出來(lái)給大家參考。第一步看失敗截圖不要急著猜。Playwright失敗時(shí)會(huì)自動(dòng)截圖先看截圖里頁(yè)面是什么狀態(tài)——是空白頁(yè)、是彈了報(bào)錯(cuò)、還是停留在上一步?jīng)]跳轉(zhuǎn)。這一眼能過(guò)濾掉一半的問(wèn)題。第二步看trace。如果截圖看不出原因打開(kāi)trace看操作回放重點(diǎn)看失敗前最后幾步的DOM狀態(tài)和網(wǎng)絡(luò)請(qǐng)求。如果某個(gè)接口返回了500那就是后端問(wèn)題如果接口正常但斷言失敗那就是前端渲染問(wèn)題。第三步看執(zhí)行日志??刂婆_(tái)的報(bào)錯(cuò)信息有時(shí)候會(huì)直接給出線索比如Failed to fetch說(shuō)明網(wǎng)絡(luò)層有問(wèn)題Cannot read properties of undefined說(shuō)明前端JS異常。這三步走完90%以上的問(wèn)題都能定位到具體層級(jí)。我強(qiáng)烈建議大家養(yǎng)成這個(gè)習(xí)慣而不是一看到失敗就重跑一遍——重跑一百遍也只能碰到運(yùn)氣好的時(shí)候通過(guò)真正的問(wèn)題始終沒(méi)有暴露。6.3 前端點(diǎn)擊無(wú)效的經(jīng)典案例復(fù)盤(pán)有一個(gè)問(wèn)題我想單獨(dú)拿出來(lái)說(shuō)因?yàn)閹缀趺總€(gè)團(tuán)隊(duì)都會(huì)遇到按鈕明明存在點(diǎn)擊了卻沒(méi)有反應(yīng)。這個(gè)問(wèn)題的根源往往是元素被某個(gè)透明遮罩層擋著或者元素看起來(lái)在頁(yè)面上、但被判定為不可點(diǎn)擊。Playwright的click會(huì)自動(dòng)檢查元素是否接收事件如果檢測(cè)到遮罩會(huì)把click攔截就會(huì)報(bào)element is not receiving events。我踩過(guò)最深的一次坑是在一個(gè)彈窗組件里彈窗關(guān)閉按鈕點(diǎn)擊之后關(guān)閉動(dòng)畫(huà)還在執(zhí)行此時(shí)頁(yè)面上殘留一層透明的遮罩層再點(diǎn)其他按鈕就會(huì)被攔截。排查了很久才發(fā)現(xiàn)是動(dòng)畫(huà)時(shí)長(zhǎng)的問(wèn)題。解決方案很土但很有效點(diǎn)擊之前先斷言遮罩層已經(jīng)消失。await page.locator(.modal-mask).waitFor({ state: hidden });這一行加下去問(wèn)題沒(méi)有再出現(xiàn)過(guò)。這類(lèi)問(wèn)題提醒我一點(diǎn)e2e測(cè)試不只驗(yàn)證業(yè)務(wù)功能它還在驗(yàn)證用戶體驗(yàn)的真實(shí)反饋。動(dòng)畫(huà)未結(jié)束、按鈕變灰、加載中狀態(tài)這些在真實(shí)用戶眼里都是點(diǎn)不了的現(xiàn)象。測(cè)試把這些問(wèn)題暴露出來(lái)對(duì)業(yè)務(wù)是好事。7. 一些經(jīng)驗(yàn)之外的個(gè)人體會(huì)做了幾年e2e之后我越來(lái)越覺(jué)得它不是一個(gè)純技術(shù)工作。寫(xiě)用例的時(shí)候要想業(yè)務(wù)是怎樣的買(mǎi)商品要走多少步用戶什么時(shí)候會(huì)退出系統(tǒng)提示應(yīng)該出現(xiàn)在哪里排查問(wèn)題的時(shí)候要判斷是自己代碼還是前端邏輯還是后端服務(wù)。它逼著人從代碼能不能跑往系統(tǒng)能不能用的方向思考。所以如果你問(wèn)我e2e測(cè)試框架里哪個(gè)功能最值得投入時(shí)間學(xué)習(xí)我的答案不是選擇器、不是斷言API、不是并行執(zhí)行而是讀懂一條業(yè)務(wù)鏈路的能力。工具只是實(shí)現(xiàn)手段真正決定e2e項(xiàng)目?jī)r(jià)值的是你對(duì)業(yè)務(wù)的拆解能力。把一條業(yè)務(wù)鏈路拆清楚哪里是核心、哪里是分支、哪里可以妥協(xié)比掌握任何框架的細(xì)節(jié)都重要。最后分享一個(gè)實(shí)用小技巧e2e用例命名不要用模塊名要用用戶故事。比如用戶可以用優(yōu)惠券抵扣訂單金額就比coupon_order_test更容易讓人理解這條用例為什么存在。多人協(xié)作的時(shí)候其他同事看到用例名就知道這個(gè)測(cè)試在整個(gè)系統(tǒng)里保護(hù)的是什么。這個(gè)習(xí)慣的成本幾乎為零收益卻能持續(xù)很久。