等待策略:為什麼 sleep 是壞味道

#pytest #顯式等待 #flaky #QA 實戰

測試在本機跑得過、CI 跑不過,幾乎每次查下去,罪魁禍首都是同一行:time.sleep(2)。睡兩秒夠嗎?本機夠,CI 不夠;改成三秒?每條測試都多白等一秒,五十條就多花一分鐘。 為什麼 sleep 壞 time.sleep 是固定等待:你賭的是「這個時間點 UI 一定好了」。問題是: 太短 → flaky :網路慢、裝置效能差、CI 機器繁忙,畫面還沒渲染完,下一個操作就撲空。 太長 → 浪費 :UI 在 0.3 秒就好了,你還在睡兩秒,整個套件時間直接膨脹。 壞例: 這段程式碼沒辦法告訴你「它在等什麼」,只知道它在賭。 顯式等待 顯式等待的邏輯是: 等到條件成立才繼續,超過上限才報錯 。條件由你定義,不是靠秒數猜。 常用條件速查: | 條件 | 用途 | | | | | visibility of element located | 等元件可見(不遮擋、非 hidden) | | element to be clickable | 等元件可點擊(可見且 enabled) | | presence of element located | 等元件出現在 UI tree(可能不可見) | | invisibility of element located | 等 loading 消失 | 隱式等待的陷阱 Appium 提供了另一種全域設定: 聽起來方便,實際上有兩個坑: 1. 全域生效,無法針對單一操作控制 :你想等某個元素最多 3 秒、另一個最多 15 秒,implicit wait 沒辦法做到。 2. 與顯式等待混用會疊加 :WebDriverWait(driver, 10) 裡面的 find element 受 implicit wait 影響,實際 timeout 可能比你設的還長,debug 時完全摸不著頭緒。 建議: 統一用顯式等待,implicitly wait 設 0 或完全不設。 收進 BasePage 等待邏輯不該散在每條測試裡,也不該散在每個 page 的每個方法裡。BasePage 那篇示範過的 BasePage.tap 和 BasePage.type 本身就已內含等待,這就是集中的好處。 子 page 用 self.tap、self.wait until visible,完全不碰 WebDriverWait;timeout 要調,改 BasePage. init 一行就全套生效。 老實說:sleep 沒辦法完全戒掉 講了這麼多 sleep 的壞話,我得承認:我的專案裡還有 sleep,而且有些是刻意留的。 原因在 Android。UiAutomator2 在裝置上是單執行緒處理指令的,連續送指令時,指令之間會互相搶。截圖完馬上找圖、找到馬上點、點完馬上驗證,在 iOS 上沒事,在 Android 上就可能點了沒反應,嚴重時整個 server 崩掉。這種情況顯式等待幫不上忙:元件早就出現了,是裝置還沒消化完上一個指令。 所以我的規則不是「禁止 sleep」,而是 禁止寫死秒數的 sleep 。需要緩衝時,一律從一份常數檔取: | 常數 | 秒數 | 用途 | | | | | | WAIT BRIEF | 0.2 | 連續低階指令之間 | | WAIT TAP | 0.3 | 點擊前後緩衝 | | WAIT ANIMATION | 0.5 | 動畫、轉場 | | WAIT ELEMENT APPEAR | 1.0 | 按鈕、彈窗出現 | | WAIT PAGE LOAD | 2.0 | 頁面載入(含網路) | | WAIT IAP | 5.0 | 系統付款對話框 | 常數有兩個好處。第一,每個 sleep 都有名字,讀程式的人知道它在等什麼。第二,要調整只改一個地方。我的經驗值是 iOS 可以乘 0.8,Android 低階機要乘 1.5。 盤點的時候,專案裡的 sleep 大部分已經改用常數。真正要還的債是剩下那些寫死數字的,而且大多集中在底層的工具程式裡,page 物件裡只剩少數幾處。 那道 lint gate,是為了擋 AI 才加的 有了常數檔,還需要有東西守著,不然規則很快就被忘掉。 真正讓我下定決心加檢查的,是 AI。我用 AI 幫忙寫和修腳本,它一遇到不穩定,第一個反應就是塞一行 time.sleep(2)。測試確實會過,但每過一次,專案裡就多一顆定時炸彈。我在 review 時一直刪,它下次還是照塞。 所以我寫了一支檢查腳本,接進 CI 的 lint 步驟,在 PR 時觸發。它有兩個設計,是我覺得最有用的地方: 只檢查新增的行。 存量的那些完全不碰,不會一開 gate 就整片紅、逼你先還完所有債才能提交。債停在原地,之後每次順手改一點。 留一個逃生口。 真的需要寫死秒數時,在該行結尾加上 sleep ok,宣告這是刻意的,就能通過。 逃生口很重要。沒有逃生口的規則,最後只會被整條關掉。有了逃生口,每個例外都會留下一行說明,review 時一眼就看得到。 加了 gate 之後,AI 塞進來的 sleep 會直接在 CI 被擋下來。它不會記得我在 review 時說過什麼,但每次都會被同一道檢查擋住。 高風險的點擊,點完要驗證 最後一個跟等待有關的習慣。有些畫面點下去不一定有反應:彈窗、日期選擇器,還有用跨平台 UI 框架畫的畫面,偶爾會丟失觸控。這時候再多的等待都沒用,因為點擊根本沒發生。 我的做法是,這類高風險點擊之後,一定要驗證「下一個畫面的元素有沒有出現」,沒出現就再點一次。等待是為了確認畫面準備好了,驗證是為了確認動作真的發生了,兩件事都要做。