開始之前——這已經是我們的第八天!!!希望你從我的文章中學到一些東西,特別是如果你和我一樣是 QA 新手,或只是單純對這個領域感到好奇。今天,我想嘗試一些不同的東西。身為 QA 工程師,我們的目標是修好東西,對吧?但如果我告訴你:在修好它之前,你得先弄壞一些東西呢? ![]()
傳統測試問的是,「系統能運作嗎?」 突變測試問的是一個更令人不安的問題:「如果系統稍微有錯,我們的測試真的會察覺嗎?」
這個區別很重要,因為通過的測試套件只能證明預期路徑被執行過,卻不能證明其斷言足夠靈敏、能偵測到不正確的行為。一個測試可能抵達了某個計算、API 端點或 UI 元件,卻在重要規則被違反時仍然通過。突變測試透過刻意引入小型缺陷——突變體(mutants)——來挑戰證據的強度,並檢查測試是否會失敗。
作為一個仍是團隊新手的成員,我的測試邊界是可觀察的系統:UI 與前端行為、透過應用程式呈現的後端行為、API、網路請求與回應、資料狀態、權限、整合,以及其他黑箱或灰箱介面。這並不代表突變測試的原則與我無關。我不必親自修改原始碼中的運算子,而是可以把同樣的推理應用到我的測試上:如果開發者不小心改動了這條規則,我的哪一個測試會偵測到?
想像 Aurora Shop 在購物車達到 $500 時提供免運。一般 QA 測試會建立一個 $500 的購物車,檢查運費變成免運,然後通過。這驗證了預期行為——但想像一個假設的突變:如果實作不小心把邊界從 >= 500 改成了 > 500 呢?
如果測試套件只測 $600,一切都維持綠燈。功能被覆蓋了,結帳流程成功執行,免運也出現了。然而,剛好 $500 的邊界已經壞掉。
這就是刻意以缺陷思維思考的價值。我不需要親自修改底層的比較邏輯,我可以圍繞 $499、$500 和 $501 設計黑箱測試,並追問可觀察的行為是否會讓那個突變現形。目標不是為了破壞而破壞,而是挑戰我們的證據是否能辨認出一個合理可能發生的缺陷。
同樣的推理也適用於 API 層級。如果某個端點應該拒絕未經授權的退款請求,我可以問:如果授權檢查被不小心移除了呢? 一個有效的已驗證請求無法回答這個問題;一個未驗證或跨帳號的請求可以。突變思維因此把測試設計的問題從*「我該測什麼?」轉變為「這個測試必須有能力抓到哪些現實中可能發生的錯誤?」*
並非每個突變都會產生可觀察的行為差異。這類突變通常被稱為等價突變體(equivalent mutants)。當沒有有意義的行為差異可供偵測時,測試無法合理地「殺死」一個突變體。
從我目前的 QA 位置來看,這個區別特別重要。在沒有原始碼存取權的情況下,我不應該把開發者回報的每一個存活的突變體,都解讀成「我漏掉了某個測試」的證明。有用的對話反而是:預期中應該有什麼可觀察的行為差異?
如果團隊能指出那個行為,我就能圍繞它設計 UI、API、邊界、授權或整合測試。如果沒有任何有效輸入能產生不同的預期結果,那個突變體可能是等價的,而不是 QA 不足的證據。
因此,突變測試應該引發調查,而不是責備。存活的突變體首先是一個問題,其次才是測試缺口。
這正是突變測試特別有價值的地方。想像 Aurora Shop 的折扣模組同時計算會員折扣與購物車滿額折扣,但必須只套用較優惠的那一個。自動化套件執行了所有相關分支,讓該模組擁有極佳的程式碼覆蓋率。
現在想像一個突變改動了選擇邏輯,讓較小的折扣勝出。如果所有既有測試依然通過,問題不在於程式碼從未被執行,而在於沒有任何斷言足夠強力,能把正確的行為與錯誤的行為區分開來。
那就是一個存活的突變體。
從 QA 的角度,我可以把這個發現轉換成可觀察的測試。建立一個會員折扣明顯小於購物車滿額折扣的購物車,透過 UI 或 API 觸發結帳,並驗證實際選用的折扣與最終收取的金額。然後把兩個數值對調,再重複一次。
這說明了為什麼覆蓋率與驗證回答的是不同的問題。覆蓋率問的是執行是否抵達了那段邏輯;突變測試問的是改動那段邏輯是否會讓證據失效。
綠燈或許能告訴我們那一行被執行過。被殺死的突變體則給出更強的證據,證明測試真的在乎那一行做了什麼。
突變框架可能為程式碼庫產生大量變體,並反覆對它們執行測試。每次變更後都對整個應用程式執行所有可能的突變,可能消耗大量 CI 時間與運算資源。
實務上的答案不一定是「更多突變測試」,而是以風險為基礎的突變測試。
如果一個 pull request 改動了折扣計算,突變分析可以集中在該變更涉及的檔案。團隊也可以優先處理那些未被察覺的缺陷會造成更大後果的模組:付款、授權、定價、退款、客戶資料,或其他業務關鍵邏輯。
這與 QA 的風險分析自然契合。低影響標籤中的一個錯字,不值得獲得與「決定向顧客收取多少錢的計算」相同的突變測試投資。
| 假設的突變 | 可能出什麼錯 | 我如何在沒有程式碼存取權的情況下測試 | 能「殺死」它的證據 |
|---|---|---|---|
免運 >= $500 變成 > $500 |
剛好 $500 被錯誤收取運費 | 測試 $499、$500、$501 的購物車 | 只有 $499 被收取運費 |
| 較大折扣變成較小折扣 | 顧客拿到錯誤的促銷 | 建立讓每種折扣各自勝出的輸入 | UI/API 回傳數學上較大的有效折扣 |
| 折扣變成可疊加 | 顧客同時拿到兩種促銷 | 讓一個購物車同時符合兩條規則 | 只出現一種折扣,且總額符合較優惠的折扣 |
| 授權檢查被弱化 | 其他帳號可以存取訂單 | 發送同帳號與跨帳號的 API 請求 | 已授權的請求成功;跨帳號請求被拒絕 |
| 退款拒絕變成核准 | 不符合資格的訂單也能退款 | 透過可用的介面提交不符合資格的訂單 | 退款維持被拒絕,且訂單狀態不變 |
| 錯誤處理變成成功回應 | 失敗的操作看起來像成功 | 觸發一個已知的無效請求 | API 回傳指定的錯誤,且沒有意外的狀態變更 |
對我而言,重要的欄位不是假設的原始碼突變本身,而是能區分正確系統與被突變系統的可觀察證據。這在能執行程式碼層級突變測試的開發者,與從外部驗證系統的 QA 工程師之間,架起了一座有用的橋樑。
無論你來自哪個領域,我相當確定你並非完全沒有套用過突變測試的紀律,只是以另一種方式或方法罷了。開發者可能突變實作中的運算子、條件、回傳值與分支;我則可以透過 UI 狀態、API 回應、邊界、權限、計算、資料轉換與整合,挑戰最終呈現的行為。
無論在程式碼邊界的哪一側,最強而有力的問題始終相同:
「如果這條規則悄悄出錯了,哪一個測試能證明它?」
如果我們能指向某個測試,並確切說明它為什麼會失敗,我們就擁有了有意義的證據。如果答案只有*「那個檔案已經有覆蓋率」或「CI 是綠的」*,那麼突變測試就揭露了一件值得調查的事——不一定是產品中的缺陷,而是我們信心中的可能缺陷。