談到軟體測試策略,大家幾乎都從覆蓋率說起,而覆蓋率偏偏是整個領域裡資訊量最低的一個數字。一個覆蓋率達到九十的程式碼庫,照樣可能在最常被走到的路徑上把缺陷送上線,因為覆蓋率量的是測試執行期間有哪些程式碼行被跑過,而不是有沒有針對這些行做出任何有意義的斷言。
真正信任自己測試套件的團隊,並不是覆蓋率百分比最高的那一群。他們是這樣的團隊:東西真的壞掉時測試會亮紅燈,其餘時候測試安安靜靜。這個特性,比多數人想像的更難用錢買到。
對任何一個測試都值得問的問題: 如果它失敗了,我知道該怎麼辦嗎?因為行為改變而失敗的測試,會告訴你一些事情。因為某個實作細節換了位置而失敗的測試,只能告訴你有人做了重構。這種失敗累積到一定數量,團隊就不再讀失敗訊息,而是一再重跑管線,直到它變綠為止。
為什麼覆蓋率會誤導人
覆蓋率回答的是這一行有沒有被執行過,而不是這一行對不對,兩者完全是兩回事。一個只呼叫函式、什麼都不斷言的測試,產生的覆蓋率跟一個逐條檢查結果分支的測試一模一樣。
這道縫隙會帶來實際後果。把目標釘在某個固定百分比上,必然生出一堆為了湊數字而寫的測試:對毫無邏輯的取值方法寫窮舉案例,而分支難以架設的付款路徑上什麼都沒有。數字上去了,風險一動也不動。
覆蓋率只在一個方向上有用。關鍵區域的覆蓋率偏低,是值得馬上處理的真實訊號。整體覆蓋率高則證明不了任何事,把它當成目標而不是診斷工具,正是團隊最後手握幾千個測試卻毫無信心的標準路徑。
軟體測試策略:哪些測試對得起它的成本
每個測試既是資產,也是負擔。它需要維護,它拖慢整個套件,而且它偶爾本身就是錯的。真正有用的問題是:哪些測試能把這份代價賺回來。
單元測試在邏輯確實複雜、而且獨立於基礎設施時最划算:計價規則、日期處理、權限檢查、各種剖析器。它們快、精準,而且能在重構中存活,因為它們描述的行為是真實存在的。
整合測試的回報比多數團隊預期的高得多,因為線上缺陷大多住在邊界上,而不是函式內部。那條對著模擬物件跑得通、對著真實資料庫就掛掉的查詢。那個選填欄位在實際資料裡從來不出現的 API。這類測試比較慢,而且值得。
端對端測試只對少數幾條旅程划算,數量應該少到可以一口氣念出來。註冊、下單,還有你的事業真正在做的那一件事。它們慢、脆、貴,一套兩百個端對端測試,會是一個團隊痛苦的主要來源。
多數程式碼庫最後收斂到的形狀是:大量單元測試、一層扎實的整合測試,再加上少數幾條端對端旅程,也就是 Martin Fowler 描述的測試金字塔 。團隊出錯的地方通常在中間那一層:他們有單元測試,也有端對端測試,卻幾乎沒有任何東西在檢查各個零件到底拼不拼得起來。
不穩定測試是信任問題
一個每二十次執行就失敗一次的測試,比沒有這個測試還糟,原因與其說是技術性的,不如說是行為性的。
只要套件裡混進幾個不穩定測試,團隊就會學到:紅色不一定代表壞掉。重跑變成例行動作。接著,一次真正的失敗也被重跑了,等它在第三次嘗試通過,就有人把它合併了進去。這時套件已經不再作為訊號發揮作用,卻還在繼續吃掉時間。
請把不穩定當成與線上缺陷同等優先的瑕疵來處理。先立刻把那個測試隔離,讓管線誠實地變綠,然後不是修好它,就是刪掉它。常見成因是測試之間共用狀態、對真實時間的依賴,以及依賴執行器根本不保證的執行順序。
刪掉一個不穩定測試,是完全正當的結局。沒有人信任的測試並沒有提供任何保護,把它移走至少能停止它繼續吃掉注意力。
測行為,不要測實作
測試套件之所以昂貴,最常見的原因是測試被綁在程式碼怎麼做上,而不是綁在程式碼做了什麼上。
把每個相依項都換成模擬物件,再斷言某個特定方法有以特定引數被呼叫過,這樣寫出來的測試會在任何一次重構之後失敗,不管行為有沒有改變。這正好是反過來的:重構才是你最希望套件告訴你什麼都沒壞的時刻,結果它卻丟給你五十筆得逐一手動排查的失敗。
替代做法是對結果做斷言。給定這樣的輸入,系統產出這樣的輸出,或者進入這樣的狀態。這類測試能在內部實作被整個重寫之後存活下來,也就代表它們恰好會在風險最高的那些改動中持續保護你。
模擬物件真正配得上位置的地方是真實邊界:金流服務商、郵件服務,以及任何很慢或帶有你不能在測試執行中真的觸發的副作用的東西。在你自己的程式碼內部,它們通常付出的比拿回來的多。
讓它在 CI 裡跑得動
沒有人願意等的套件,就是會被略過的套件。如果完整執行要花四十分鐘,大家推完程式碼就走開,回饋會在他們已經開始做別的事情之後才姍姍來遲。
把它拆開。快速的單元測試與整合測試跑在每一次推送上,幾分鐘內給出答案。緩慢的端對端旅程放到合併時或排程執行。這跟我們在2026年英國開發團隊CI/CD最佳實踐 指南裡談的部署紀律,是同一套道理。
讓失敗訊息可讀。一則只說某個斷言為假、卻完全沒提當時在檢查什麼的失敗,每次都得花掉十分鐘的考古工作。用測試所保護的行為來命名測試,失敗清單就會直接變成一份「什麼壞掉了」的說明。
還要讓套件保持確定性。不做真實網路呼叫,不在沒有控制的情況下依賴今天的日期,不對執行順序做任何假設。每一個非確定性的測試,都是一個還沒發作的不穩定測試。
面對一個沒有測試的程式碼庫,該從哪裡開始
不要試圖事後補齊完整覆蓋率;投入極其龐大,而其中大半保護的是根本沒人再改的程式碼。
從缺陷會造成金錢損失的路徑開始,先圍繞它們寫整合測試,因為以每寫一個測試的收益來算,這類測試抓到的問題最多。接著,在每一次修缺陷時補一個測試,而且動手修之前先把失敗重現出來。這樣一來,覆蓋率會精準地長在缺陷真正發生的地方,而這正是關於風險藏在哪裡的最佳訊號。
Mecanik 在定制軟體開發服務 中會檢視並建立測試策略,通常從一個問題開始:哪些失敗真的會痛。如果你的套件很大,團隊上線時卻還是提心吊膽,問題很少出在測試的數量上。
延伸閱讀: 真的有人讀的技術文件 、第一週就能交付的開發者導入流程 、真正能改變什麼的事故檢討 、API 版本管理:何時打破相容,如何避免 。
常見問題
高測試覆蓋率是一個好目標嗎? 單靠它並不是。覆蓋率量的是測試執行期間有哪些程式碼行被跑過,而不是有沒有針對這些行做出任何有意義的斷言,所以一個只呼叫函式、什麼都不斷言的測試,得分跟一個逐條檢查分支的測試完全相同。關鍵路徑上覆蓋率偏低是有用的訊號;整體百分比很高則證明不了多少事。
單元測試、整合測試與端對端測試的正確比例是什麼? 對確實複雜的邏輯寫大量單元測試,配上一層份量十足的整合測試,因為線上缺陷大多住在邊界上,再加上少數幾條你能一口氣念出來的端對端旅程。多數團隊錯在中間層:他們有單元測試和端對端測試,卻幾乎沒有東西在檢查各個零件拼不拼得起來。
我該怎麼處理不穩定測試? 把它們當成與線上缺陷同等優先的瑕疵。先立刻隔離那個測試,讓管線保持誠實,然後不是修好它,就是刪掉它。只要套件裡混進幾個不穩定測試,團隊就會學到紅色不代表壞掉,於是反射性地重跑,最後把一次真正的失敗也合併了進去。刪掉一個不穩定測試是完全正當的結局。
測試裡應該模擬相依項嗎? 在真實邊界上應該:金流服務商、郵件服務,以及任何很慢或帶有副作用的東西。在你自己的程式碼內部,模擬物件通常付出的比拿回來的多,因為斷言某個特定方法有以特定引數被呼叫過,會讓測試在任何一次重構之後失敗,不管行為有沒有改變。
要怎麼替一個完全沒有測試的程式碼庫補上測試? 不要試圖事後補齊完整覆蓋率。圍繞缺陷會造成金錢損失的路徑寫整合測試,因為以每寫一個測試的收益來算,這類測試抓到的問題最多。接著在每一次修缺陷時補一個測試,並在動手修之前先把失敗重現出來,這樣覆蓋率就會精準地長在缺陷真正發生的地方。
評論