iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0

寫得糟糕的測試不總是以明顯的方式失敗。事實上,更危險的版本恰恰相反:它持續通過、讓 CI 保持綠燈,並給予團隊未經掙得的信心。這在 AI 起草的測試中尤其值得注意。AI 非常擅長重現成功測試的外觀 /images/emoticon/emoticon45.gif ——準備、執行、斷言、通過——但結構完整的測試不必然是有意義的驗收證據。

我把最常見的三種模式視為假綠燈:執行卻沒有有意義的斷言、斷言實作細節而非行為,以及讓過於寬泛的 mock 取代測試本應驗證的行為。它們就像一支探棒壞掉、卻仍顯示平靜穩定讀數的三用電表。顯示幕看起來可信;問題在於它已經不在測量電路了。

情境

在先前的測試中,Aurora Shop(虛構)一直作為一個簡單的電子商務系統,用來檢視 QA 證據如何能比一個通過的結果更有力。我們測試過折扣優先序、免運邊界、退款資格與 AI 協助的客戶支援等規則,然後以邊界分析與突變測試思維挑戰那些測試:如果行為被悄悄改動,我們的測試會察覺嗎? 這些練習確立了執行程式碼與真正驗證行為之間的重要區別。有了這個基礎,下一個問題是:當一個測試看起來正當、持續通過,但本質上很脆弱時,會發生什麼——也就是三種假綠燈。

同樣的三種形狀,換到店裡的另一個角落。每個範例都先呈現 AI 起草的測試,再呈現真正能保護些什麼的版本。

1. 假綠燈一:執行卻沒有斷言

AI 被要求涵蓋運費的計算。它「涵蓋」的方式是呼叫函式,讓測試執行轉為綠燈。

def test_shipping_fee():
    shipping_fee(cart_total=500, region="TW")   # 被呼叫過,所以算「測過」

沒有斷言的測試沒有失敗的可能,所以如果 >= 500 悄悄漂移成 > 500,CI 保持綠燈,而剛好 $500 的購物車卻被收取運費。修法是把運費明確表述為一個值——在邊界上,以及邊界下一步之處:

def test_shipping_fee():
    assert shipping_fee(cart_total=500, region="TW") == 0
    assert shipping_fee(cart_total=499, region="TW") == 60   # (演練資料)

2. 假綠燈二:斷言實作細節

Aurora Shop 會套用會員折扣與優惠券,取省較多者,絕不同時疊加。AI 起草的測試斷言的是內部步驟順序,而不是顧客看得到的結果。

def test_pricing_order():
    cart = Cart(total=1500, member=True, coupon="SAVE100")
    cart.apply_pricing()
    assert cart.steps_taken == ["member", "coupon"]   # 食譜,不是那道菜

把兩個運算在內部對調,最終價格不變——但這個測試卻會壞掉。更糟的是,如果一個臭蟲讓兩種折扣疊加,步驟清單看起來依然整齊,測試就在行為改變的情況下保持綠燈。修法是把斷言提升到輸入與輸出的對應關係:

def test_pricing_order():
    cart = Cart(total=1500, member=True, coupon="SAVE100")
    assert cart.payable() == 1200   # 會員 20%(−300)勝過 −100 優惠券

3. 假綠燈三:過於寬泛的 mock 掩蓋真實行為

這就是第三天保持沉默的那個服務。AI 起草的測試把 mock 設定成對每筆訂單都回覆「可退」,所以這個測試永遠抓不到說相反規則的政策。

def test_return_request():
    policy = MagicMock()
    policy.decide.return_value = "returnable"   # 對任何訂單都回「可退」
    assert handle_return(order_id="A1", policy=policy) == "approved"

這只證明了 handle_return 會轉發 policy 說的話。唯一值得知道的事——政策是否依訂單類別作答——被 mock 吞掉了。修法是讓 mock 依輸入而定,或者乾脆丟掉它,改跑真正的規則表:

def test_return_request():
    policy = RuleBasedPolicy(rules={"apparel": "returnable",
                                    "perishable": "non-returnable"})
    assert handle_return(order_id="A1", policy=policy) == "approved"   # 服飾
    assert handle_return(order_id="A2", policy=policy) == "rejected"   # 生鮮

每個修好的測試都以同樣的方式贏得它的綠燈:改變輸入,斷言必須跟著移動;弄壞規則,測試必須轉紅。

假綠燈檢查清單

模式 症狀 修法 如何驗證修復
執行,無斷言 沒有斷言,或只有「不為 None」 斷言金額、優先順序、錯誤訊息 故意弄壞實作;測試應轉紅
斷言實作細節 無害重構會弄壞它,行為改變卻不會 把斷言上移到輸入與輸出 做一次無害的重構;測試應保持綠燈
過於寬泛的 mock mock 的輸出與輸入無關 縮小 mock 範圍,或改用真實實作 改變輸入;斷言應跟著移動
覆蓋率膨脹 覆蓋率上升,但缺失的斷言依舊缺失 對照分支清單與風險清單核對 與第 08 天的存活突變體比對
只測快樂路徑 沒有零、空購物車或上限的案例 從邊界清單補上案例 這些案例在舊程式碼上應該失敗

假綠燈檢查清單把這些想法轉化為一個實用的方法,用來挑戰一個通過的測試是否真的提供了有用的證據。它把每種脆弱的測試模式對應到其症狀、一種可能的修正,以及——最重要的——驗證該修正的方法。執行卻沒有有意義斷言的測試,應該以故意弄壞預期行為的方式來挑戰;依賴實作的斷言,應該在無害的重構下存活;而 mock 應該在輸入改變時給出有意義的回應,而不是回傳預先決定的答案。這份清單也把同樣的推理延伸到覆蓋率膨脹與只測快樂路徑,用存活的突變體與邊界案例,揭露綠燈的 CI 流水線可能掩蓋的缺口。簡言之,這張表把紅燈也視為證據:一個好的測試,應該在無關的實作細節改變時保持綠燈,卻在它所保護的行為真正壞掉時可靠地轉紅。


上一篇
我故意弄壞它:突變測試
下一篇
我們保留的測試,我們退役的測試
系列文
踏入品質保證工程之路:QA 新手如何善用 AI 與開發者工具 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言