開發日誌 · 2026-07-24

2026-07-24#e2e-testing#appium#test-design#mocking#ios

我找到一個破口,然後親手證明它不存在

有一張測案要驗這件事:金額過了前一關、但在最後確認那關才被擋下來(額度不足)。它一直被 skip,理由寫的是「沒辦法只在確認關卡觸發不足」。今天我想把它救回來。

翻 mock 的時候我眼睛一亮。這條流程其實跨了兩份後端資料:輸入金額時先做試算,走一份;到最後確認那關,走另一份。而這兩份給的可用額度不一樣——試算看到的比較寬,確認看到的比較緊。

那不就成了嗎。挑一個卡在中間的金額:比試算的額度小,所以試算會放行;比確認的額度大,所以確認會擋。不用改 mock、不用重編 App,純靠一個輸入值就能造出那個「只在確認關卡不足」的狀態。

我還特地把兩個數字都查出來對過,確認中間真的有一段空間可以塞。寫了個探針,輸入那個中間值,一路走到確認頁,準備 dump 那個「額度不足」的彈窗、把 locator 撈出來。

跑完一看——探針卡在金額頁。連下一頁的欄位都還沒出現。

愣了一下。金額頁就把我擋掉了,代表金額頁的輸入上限也是那個比較緊的額度,不是我以為的試算額度。換句話說,兩關其實共用同一個上限;試算那份資料裡比較寬的數字,根本沒被拿去當輸入上限。我腦裡那個「中間金額」的空間,不存在。

所以這張測案,以現在的工具真的造不出來——要嘛 App 改成兩關用不同上限,要嘛得有個能記狀態的 mock(第一次放行、第二次才擋)。原本的 skip 理由沒寫錯。只是它當初是用猜的,今天我用一次真跑把它變成「驗過了」。

有點嘔。但這比「想當然耳把它救回來、結果根本沒測到那個情境」好太多。假的綠燈比紅燈危險。

helper 丟了例外,可是它其實成功了

第二組測案,要驗「主要的付款方式被停用時,畫面會自動落到另一種付款方式的分頁」。這個情境的 mock 早就有了,連對應的資料 profile 都現成,我以為十分鐘搞定。

結果送出金額那一步就炸了。

我們有個共用 helper,大概長這樣(示意):

async function goNext() {
  await amountField.setValue(...)
  await nextButton.click()
  await primaryField.waitForDisplayed({ timeout: 10000 })  // 確認前進成功
}

它靠「主要方式的欄位出現」來確認「頁面前進成功了」。平常沒問題——正常路徑本來就會走到有那個欄位的頁面。

但我這個情境,主要方式是停用的。畫面自動落到另一種付款方式的分頁,那頁沒有那個欄位。於是 waitForDisplayed 等了十來秒、等不到,throw。

第一反應是 flaky,重跑。一樣的地方炸。

改成分段 dump 才看清楚:送出之後,頁面其實已經前進了——當前畫面已經是另一種付款方式的頁面,連那條分頁上該有的欄位、選項都渲染好了。前進這件事,從頭到尾都成功。失敗的只有 helper 拿來當「成功訊號」的那個等待條件:它在等一個在這條路徑上永遠不會出現的元素。

看懂之後就簡單了。這條路徑不該拿主要方式的欄位當訊號:

await goNext().catch(() => {})       // 前進本身會成功,只是它等的訊號不在這條路上
await altField.waitForDisplayed()    // 改用這條路徑真正會出現的欄位當訊號

catch 吞掉那個「等錯東西」的逾時,再用這條分頁自己的欄位重新確認到站。

寫的時候我猶豫了一下,要不要去改 helper——讓它同時接受兩種訊號之一。想想算了。那個 helper 服務的是主線,主線行為沒錯;為了一條分支去讓共用的東西變複雜,不划算。分支的特殊性,留在分支的測案裡處理就好。

真正咬到我的不是那十幾秒逾時,是我看到 throw 的第一秒就認定「這步失敗了」。throw 只代表 helper 沒等到它想等的東西,不代表你要它做的事沒做成。這兩件事,今天差點被我畫上等號。

← 回部落格