本文是「測試之外的那一半:一個 QA 回頭帶當年的自己」系列第 23 篇。
實習的時候,我的腳本有幾個步驟三不五時會失敗。
我的處理方式是加 retry。失敗了就重跑一次,第二次通常會過,報表就綠了。
那招有效。而且因為它有效,我用了很久才發現我在做什麼。
一個測試偶爾失敗,這件事本身是資訊。
它在說:在某些條件下,這個功能的行為跟平常不一樣。
加 retry 之後,那句話就消失了。報表變綠,沒有人會再去問那個「某些條件」是什麼。
我不是在修問題,我是在把問題的訊號關掉。
後來遇到一個不肯讓我蒙混過去的。
某個測試步驟,在連續跑一整輪的時候三不五時失敗一次,訊息是找不到頁面元素。但我單獨跑那一步,永遠重現不出來。
第一個線索是:App 的行程還活著。不是 crash。
第二個線索在截圖裡。失敗當下的畫面不是 App 本身,是一個客服性質的網頁,整個蓋住了畫面。
所以測試不是找不到元素。元素在那裡,只是被一層它不預期的東西蓋住了。這就是 Day 22 那張分層表最下面的覆蓋層。
找到覆蓋物還不夠。我想知道它從哪裡來——是 App 自己彈的,還是我的測試不小心點到了什麼。
這兩個答案的後果完全不同。如果是測試誤點,那是我的腳本要修。如果是 App 自己彈的,那是產品行為,而且可能會發生在真實使用者身上。
我用 logcat 追那個畫面的來源,查出發起它的 intent 屬於哪個 uid。結果是 App 自己。
再往下挖,原因很樸素:那是一個行銷推播,投放條件是「使用者累積走過幾個頁面」。
自動化跑得比人快太多,連續跑一輪就把那個累積條件湊滿了,於是推播在半路 fire 出來。單獨跑一個步驟,湊不滿,自然永遠不重現。
「單獨跑不重現、連續跑才壞」本身就是線索。 它指向累積狀態,不指向隨機。
那個推播會繼續在真實使用者的某個頁面彈出來,蓋住畫面。
它不會變成 bug 單,因為唯一固定撞到它的是我的測試,而我的測試已經被我教會了「撞到就重跑」。
這是 retry 最貴的地方。它不只是掩蓋一個測試的失敗,它讓一個產品行為永遠不會被任何人注意到。
不是所有 flaky 都藏著產品問題。
有一種很常見的:畫面用現代的宣告式 UI 框架寫,元件會在資料更新時重新產生。你抓到元素的那一刻它還在,等你要點的時候它已經被換掉了,於是報 stale element。
這種的正解是重抓一次,不是加 sleep。
sleep 是猜——猜多久夠。猜短了還是壞,猜長了每一輪都在浪費時間,而且它會讓你永遠不知道真正需要等多久。重抓是針對成因:元素被換掉了,那就重新拿一次。
兩個案例的處理方式不同,但判準是同一個:先弄清楚它為什麼壞,再決定怎麼處理。
能。但它是結論,不是反射動作。
我現在的順序是:先問「它在什麼條件下才發生」,追到成因,然後判斷那個成因能不能容忍。能容忍的才加 retry,而且要在註解裡寫清楚我容忍的是什麼。
寫下來這件事跟 Day 10 那三行關單紀錄是一樣的道理。沒有那行註解,半年後接手的人會看到一個 retry,然後完全不知道它在擋什麼——他只會知道拿掉它測試就會壞。
測試偶爾失敗的時候,先別加 retry。先問一句:
它在什麼條件下才發生?
特別注意這三種條件,它們都指向累積狀態而不是隨機:
只有連續跑才壞、只有跑到後半段才壞、只有在某台機器上才壞。
追到成因之後再決定怎麼處理。真的要加 retry,就在旁邊寫一行「我容忍的是什麼」。
明天聊:同一份腳本,我筆電跑得動,共用機器跑不動。