機(jī)制詳解:從 Worker 進(jìn)程隔離到源碼級(jí)重試調(diào)度)
Playwright 測試重試Retries機(jī)制詳解從 Worker 進(jìn)程隔離到源碼級(jí)重試調(diào)度【免費(fèi)下載鏈接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/pl/playwright本文圍繞 Playwright 官方文檔中的 Retries 指南展開先講清測試失敗時(shí) Worker 進(jìn)程如何被整體丟棄與重建再覆蓋--retries命令行、playwright.config.ts配置、test.describe.configure({ retries })分組重試、TestInfo.retry運(yùn)行時(shí)檢測以及 Serial 模式整組重試等全部官方用法并結(jié)合 runner/dispatcher.ts、common/config.ts 等倉庫源碼說明重試候選如何被收集、隔離調(diào)度與分類為 flaky 的完整鏈路。為什么需要重試測試失敗與 Worker 進(jìn)程生命周期測試重試Retries是一種在測試失敗后自動(dòng)重新運(yùn)行的機(jī)制專門用于應(yīng)對**間歇性失敗flaky**的測試。理解它的前提是理解 Playwright Test 的 Worker 進(jìn)程模型測試運(yùn)行在worker 進(jìn)程中。這些進(jìn)程是獨(dú)立的操作系統(tǒng)進(jìn)程由測試運(yùn)行器test runner統(tǒng)一編排所有 worker 擁有完全一致的環(huán)境每個(gè) worker 都會(huì)啟動(dòng)自己獨(dú)立的瀏覽器實(shí)例。文檔用一段典型用例演示了三種場景。假設(shè)存在如下測試文件import { test } from playwright/test; test.describe(suite, () { test.beforeAll(async () { /* ... */ }); test(first good, async ({ page }) { /* ... */ }); test(second flaky, async ({ page }) { /* ... */ }); test(third good, async ({ page }) { /* ... */ }); test.afterAll(async () { /* ... */ }); });場景一全部通過。所有測試按順序在同一個(gè) worker 進(jìn)程中運(yùn)行* Worker process starts * beforeAll hook runs * first good passes * second flaky passes * third good passes * afterAll hook runs場景二任一測試失敗。Playwright Test 會(huì)連同瀏覽器一起丟棄整個(gè) worker 進(jìn)程并啟動(dòng)一個(gè)新進(jìn)程。測試從下一個(gè)測試開始在新 worker 中繼續(xù)* Worker process #1 starts * beforeAll hook runs * first good passes * second flaky fails * afterAll hook runs * Worker process #2 starts * beforeAll hook runs again * third good passes * afterAll hook runs場景三啟用重試后。第二個(gè) worker 進(jìn)程會(huì)以重試失敗的測試作為起點(diǎn)然后繼續(xù)后續(xù)測試* Worker process #1 starts * beforeAll hook runs * first good passes * second flaky fails * afterAll hook runs * Worker process #2 starts * beforeAll hook runs again * second flaky is retried and passes * third good passes * afterAll hook runs這種失敗即換進(jìn)程的方案對相互獨(dú)立的測試非常有效并保證了失敗的測試不會(huì)污染健康的測試。這一設(shè)計(jì)在源碼中可以得到印證。在 runner/dispatcher.ts 中Job的onDone路徑在 job 結(jié)束后會(huì)調(diào)度剩余測試與重試任務(wù)當(dāng) worker 進(jìn)程意外退出時(shí)onExit會(huì)生成worker process exited unexpectedly錯(cuò)誤并跳過相關(guān)測試。而每個(gè) job 完成后runInWorker通過 IPC 把retry: test.results.length傳給 worker——當(dāng)前已運(yùn)行次數(shù)即本次的重試序號(hào)這保證了新 worker 從正確的重試輪次開始執(zhí)行。配置重試命令行與配置文件Playwright 支持測試重試啟用后失敗的測試會(huì)反復(fù)重試直到通過或達(dá)到最大重試次數(shù)。默認(rèn)情況下失敗的測試不會(huì)被重試。命令行方式# 給失敗的測試 3 次重試機(jī)會(huì) npx playwright test --retries3該選項(xiàng)在 program.ts 中定義官方描述為[--retries retries, { description: Maximum retry count for flaky tests, zero for no retries (default: no retries) }]即傳入0表示不重試不傳時(shí)默認(rèn)為不重試。配置文件方式import { defineConfig } from playwright/test; export default defineConfig({ // Give failing tests 3 retry attempts retries: 3, });從源碼結(jié)構(gòu)看retries的生效順序在 common/config.ts 中明確定義retries: takeFirst(configCLIOverrides.retries, projectConfig.retries, config.retries, 0),即優(yōu)先級(jí)為CLI--retries 單個(gè) project 的retries 全局config.retries 默認(rèn) 0。這意味著你可以在projects數(shù)組里為不同瀏覽器項(xiàng)目配置不同的重試次數(shù)同時(shí)仍可用命令行臨時(shí)覆蓋。結(jié)果分類passed、flaky、failed啟用重試后Playwright Test 會(huì)把測試歸為三類passed首次運(yùn)行即通過的測試flaky首次運(yùn)行失敗、但在重試后通過的測試failed首次運(yùn)行失敗且所有重試均失敗的測試。一次典型的運(yùn)行輸出如下Running 3 tests using 1 worker ? example.spec.ts:4:2 ? first passes (438ms) x example.spec.ts:5:2 ? second flaky (691ms) ? example.spec.ts:5:2 ? second flaky (522ms) ? example.spec.ts:6:2 ? third passes (932ms) 1 flaky example.spec.ts:5:2 ? second flaky 2 passed (4s)這一分類邏輯同樣體現(xiàn)在倉庫的報(bào)告器實(shí)現(xiàn)中。在 reporters/base.ts 中TestSummary包含獨(dú)立的flaky: TestCase[]字段摘要消息會(huì)把 flaky 測試以黃色高亮單獨(dú)列出${flaky.length} flaky并在失敗明細(xì)中優(yōu)先展示 flaky 項(xiàng)。因此flaky 不會(huì)導(dǎo)致整個(gè) run 失敗但會(huì)在匯總中顯式暴露方便持續(xù)追蹤不穩(wěn)定測試。運(yùn)行時(shí)檢測重試TestInfo.retry 與分組配置TestInfo.retry可以在測試內(nèi)部通過testInfo.retry屬性檢測當(dāng)前是否為重試0 表示首次運(yùn)行該屬性對任何測試、hook 和 fixture 都可用。典型用途是在重試前清理服務(wù)端狀態(tài)import { test, expect } from playwright/test; test(my test, async ({ page }, testInfo) { if (testInfo.retry) await cleanSomeCachesOnTheServer(); // ... });test.describe.configure 指定分組重試針對特定測試組或單個(gè)文件可以用test.describe.configure({ retries })覆蓋全局配置import { test, expect } from playwright/test; test.describe(() { // All tests in this describe group will get 2 retry attempts. test.describe.configure({ retries: 2 }); test(test 1, async ({ page }) { // ... }); test(test 2, async ({ page }) { // ... }); });該機(jī)制在源碼中的落點(diǎn)是 common/test.tsTestSuite持有_retries字段TestCase持有retries字段且TestCase.retries初始值為0當(dāng)配置序列化時(shí)suite 層的_retries會(huì)被展開到該組下的每個(gè)測試上最終由 dispatcher 用test.results.length test.retries 1判斷該測試還有沒有重試額度。Serial 模式整組測試一起重試使用test.describe.serial等價(jià)于test.describe.configure({ mode: serial })可以分組管理相互依賴的測試確保它們始終在一起按順序運(yùn)行。組內(nèi)任一測試失敗時(shí)其后的所有測試會(huì)被跳過而整組測試會(huì)作為一個(gè)整體一起重試。import { test } from playwright/test; test.describe.configure({ mode: serial }); test.beforeAll(async () { /* ... */ }); test(first good, async ({ page }) { /* ... */ }); test(second flaky, async ({ page }) { /* ... */ }); test(third good, async ({ page }) { /* ... */ });未啟用重試時(shí)失敗后的測試全部被跳過* Worker process #1: * beforeAll hook runs * first good passes * second flaky fails * third good is skipped entirely啟用重試時(shí)整組一起重試* Worker process #1: * beforeAll hook runs * first good passes * second flaky fails * third good is skipped * Worker process #2: * beforeAll hook runs again * first good passes again * second flaky passes * third good passes源碼印證了整組重試的調(diào)度細(xì)節(jié)。在 runner/dispatcher.ts 中onDone會(huì)為每個(gè)失敗測試向上查找最外層 serial suite_parallelMode serial把該 suite 下全部測試加入重試候選集for (const serialSuite of serialSuitesWithFailures) { // Add all tests from failed serial suites for possible retry. // These will only be retried together, because they have the same // retries setting and the same number of previous runs. serialSuite.allTests().forEach(test retryCandidates.add(test)); }同時(shí)dispatcher 還會(huì)把屬于某個(gè)已失敗 serial suite 的后續(xù)剩余測試整體標(biāo)記為跳過_massSkipTestsFromRemaining這正是文檔中 third good is skipped 的實(shí)現(xiàn)來源。注意官方建議通常更好的做法是讓測試保持相互隔離從而能夠被高效地獨(dú)立運(yùn)行和重試。Serial 模式應(yīng)作為依賴場景的例外手段。源碼視角重試如何被調(diào)度isolated 與 immediate除了文檔顯式描述的行為倉庫源碼還揭示了重試調(diào)度的兩個(gè)關(guān)鍵策略適合希望深入理解 runner 內(nèi)部機(jī)制的讀者。重試候選的收集條件在 runner/dispatcher.ts 中job 結(jié)束后的重試候選集由三個(gè)條件共同決定非可重試錯(cuò)誤的測試被排除_failedWithNonRetriableError命中的失敗測試?yán)?plan 標(biāo)注跳過等不會(huì)進(jìn)入重試重試額度檢查只有滿足test.results.length test.retries 1的測試才會(huì)被安排重試其中test.results.length即已運(yùn)行次數(shù)serial 組擴(kuò)展失敗測試所在的最外層 serial suite 會(huì)被整體納入候選。retryStrategyimmediate 與 isolatedcommon/config.ts 定義了retryStrategy配置項(xiàng)取值immediate | isolated默認(rèn)值為immediateconfigLoader.ts 對非法取值會(huì)直接報(bào)錯(cuò)。兩種策略在 dispatcher.ts 中的行為差異為for (const test of retryCandidates) { if (test.results.length test.retries 1) { // Immediate retries run together with the remaining tests, in a single job. if (this._testRun.config.retryStrategy immediate) remaining.push(test); else isolatedRetries.push(test); } }immediate重試任務(wù)與剩余未跑的測試合并在同一個(gè)新 job 中執(zhí)行運(yùn)行效率更高isolated重試任務(wù)被拆出獨(dú)立的isolatedRetriesJob只包含失敗測試避免與正常測試混合——便于隔離觀察不穩(wěn)定測試。此外program.ts 中 trace 模式還有一組與重試聯(lián)動(dòng)的取值on-first-retry、on-all-retries、retain-on-failure、retain-on-first-failure、retain-on-failure-and-retries。例如配置trace: { mode: on-first-retry }可以在首次失敗后的重試中自動(dòng)采集 trace幫助定位 flaky 根因。在 Serial 組內(nèi)跨測試復(fù)用單個(gè) PagePlaywright Test 默認(rèn)為每個(gè)測試創(chuàng)建隔離的Page對象。如果確實(shí)需要跨測試復(fù)用同一個(gè)Page可以在test.beforeAll中自行創(chuàng)建、在test.afterAll中關(guān)閉通常需要配合 serial 模式import { test, type Page } from playwright/test; test.describe.configure({ mode: serial }); let page: Page; test.beforeAll(async ({ browser }) { page await browser.newPage(); }); test.afterAll(async () { await page.close(); }); test(runs first, async () { await page.goto(https://playwright.dev/); }); test(runs second, async () { await page.getByText(Get Started).click(); });JavaScriptCommonJS版本等價(jià)寫法// ts-check const { test } require(playwright/test); test.describe.configure({ mode: serial }); /** type {import(playwright/test).Page} */ let page; test.beforeAll(async ({ browser }) { page await browser.newPage(); }); test.afterAll(async () { await page.close(); }); test(runs first, async () { await page.goto(https://playwright.dev/); }); test(runs second, async () { await page.getByText(Get Started).click(); });這種模式常用于登錄態(tài)復(fù)用等場景第一個(gè)測試完成登錄后后續(xù)測試直接復(fù)用同一瀏覽器上下文避免重復(fù)鑒權(quán)。最佳實(shí)踐小結(jié)默認(rèn)不重試按需在 CI 中開啟本地開發(fā)時(shí)保持默認(rèn)無重試以快速暴露問題CI 中用npx playwright test --retries2或配置文件兜底 flaky把 flaky 當(dāng)作信號(hào)而非噪音報(bào)告器會(huì)把 flaky 單獨(dú)計(jì)數(shù)并在匯總中展示長期 flaky 的測試應(yīng)回到隔離測試 穩(wěn)定等待的路線而不是無限加重試次數(shù)重試時(shí)清理副作用利用testInfo.retry在重試前清理緩存、數(shù)據(jù)庫或服務(wù)器狀態(tài)避免上一輪失敗的殘留影響本輪判定依賴測試用 serial 整組重試describe.serial保證失敗組內(nèi)后續(xù)測試被跳過且整組重跑但官方建議優(yōu)先改造為相互隔離的獨(dú)立測試配合 trace 模式定位 flakyon-first-retry/on-all-retries等 trace 模式與重試聯(lián)動(dòng)只記錄失敗路徑的追蹤數(shù)據(jù)控制產(chǎn)物體積。相關(guān)參考配置指南、測試分類指南、重試調(diào)度實(shí)現(xiàn) dispatcher.ts、配置解析 config.ts、報(bào)告器 flaky 分類 base.ts?!久赓M(fèi)下載鏈接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/pl/playwright創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考