軟體測試 — 如何寫出好維護的測試?
2025年7月23日
先前我們談了測試金字塔的概念 ,在這一期我們將延續這個主題,進一步來談如何寫出好測試的程式碼,以及可以如何讓測試更好維護。
如何讓測試好維護
在談完如何寫出好測試的程式碼,接著我們來談如何讓測試比較好維護。
測試的意圖
首先,要讓測試好維護,要盡可能避免測試的意圖不明,讓人讀不懂該測試到底在測什麼。在業界中有個觀點是,好的測試就像文件一樣,讓人可以透過測試來了解程式碼庫。從這角度來看,當測試無法讓人輕易看懂,該測試就相對沒有價值。
在寫測試時,有一個叫 DAMP (Descriptive And Meaningful Phrases) 的原則,是指測試要盡可能做到具描述性,用有意義的字句來提高意圖的可讀性。當這樣寫,未來的維護者看到後,就能夠用更短的時間看懂,這樣要維護起來也更容易。
避免重複
除了意圖明確的 DAMP 原則,跟寫程式一樣,如果寫測試時有照著 DRY (Don't Repeat Yourself) 的原則,盡可能避免重複,會讓測試更容易維護。舉例來說,在 [直播] 如何寫出更乾淨、好維護的程式碼? 直播中,我們有談到可以如何運用工廠方法 (factory Method) 來生成測試資料,這樣讓寫測試資料時,能夠避免寫很多重複,然後要改時要到各處去改 (對這個概念不熟的讀者,推薦回去看該直播的回放)。
另一個常見的避免重複方式,是在寫 E2E 測試時,可以把重複會有的流程給抽出來,例如前面的登入後到首頁的流程,可以抽出來。這樣在寫不同流程的測試時,就可以直接引入「登入後進到首頁」的流程,不用重複寫,而且如果「登入後進到首頁」的流程需要更改,也可以在一個地方改就好,不用四處去改,這樣維護起來也會更容易。
整理分區
除了上述兩點外,一個可以簡單讓測試好維護的方式,是有效做好整理與區分。
舉例來說,在寫單元測試時,可以透過共置 (colocation),讓測試好被維護。共置的概念在於,測試應該要跟程式碼放在一起。有些團隊會把單元測試放在 /src/test
底下,不論是哪段程式碼的單元測試,通通放在那。但比起這種作法,會更推薦把單元測試跟程式碼放在同一個資料夾底下,例如 /add/index.ts
與 /add/index.test.ts
都是放在 /add
底下。
當這樣整理的好處在於,如果今天想要透過測試來理解某段程式碼,因為在同一個資料夾,所以可以馬上找到該程式碼的測試。同樣地,如果更新完程式碼,也可以馬上找到測試的檔案然後更新;如果要刪除程式碼,要同時把測試刪除時,這樣放也很容易一起刪掉。
當然你可能會問,整合測試與 E2E 測試是不是不適合這樣? 畢竟有多個模組都跟測試有關,那應該放在哪裡? 因此會推薦在整合測試或 E2E 測試,可以用領域導向 (domain-oriented) 來分類。
舉例來說,如果在一個電商相關的 E2E 測試中,我們可以依照領域來分類,例如把商品陳列、訂單管理、金流頁面等,分在不同的資料夾,然後把相關的流程都放在一起,例如商品頁相關的流程的測試都放一起。這樣做未來要維護時,就可以很輕鬆找在相對應的領域,找到相關的測試。
以下是個具體的例子
│ │ ├── product/
│ │ │ ├── productListing.spec.js
│ │ │ ├── productDetail.spec.js
│ │ │ └── addToCart.spec.js
│ │ ├── order/
│ │ │ ├── createOrder.spec.js
│ │ │ ├── orderConfirmation.spec.js
│ │ │ └── orderHistory.spec.js
測試該覆蓋哪些案例?
在這期主題文的最後,想與大家討論一個,相信很多人都有的問題,那就是測試案例該怎麼寫? 該覆蓋哪些?
舉例來說,假如今天寫了一個叫 sortNumbersAscending
的函式,會把一個帶有數字的陣列,轉換成升冪排序。當看到這個函式,大家會寫哪些測試案例呢?
一個最直觀的方式,會是寫像是 expect(sortNumbersAscending([3, 1, 2])).toEqual([1, 2, 3])
這樣的測試案例。這種寫法是叫以範例為基礎的測試 (example-based testing),但這種寫法無法完整給開發者信心。以上面這個例子來說,即使通過了,我們能確保在 [3, 1, 2]
的測試案例下,這個函式沒問題,但這不代表在其他案例也沒問題。
以 JavaScript 來說,假如團隊有某個對 JavaScript 不熟的開發者,在寫 sortNumbersAscending
時,是這樣實作 (這邊是舉例說明,相信大家不會這樣寫)
function sortNumbersAscending(arr) {
return arr.sort();
}
在這個寫法下,因為 JavaScript 的語言特性,雖然 sortNumbersAscending([3, 1, 2]
會有正確的輸出,但是 sortNumbersAscending([10, 2, 1])
會輸出 [ 1, 10, 2 ]
,會是不符合預期的。
因為 JavaScript 的 sort
在沒有傳入比較的回呼函式時,會把陣列中的值轉換成字串來比較,才會出現這結果。但假如在寫測試案例的範例時,沒有去想到 sortNumbersAscending([10, 2, 1])
,那很可能即使全部測試案例都通過,但卻沒有抓到程式碼有問題的地方。
不過,實務上來看,很可能無法想到各種要測的極端狀況,那可以怎麼做來確保有完整覆蓋該覆蓋的案例呢?
我們在 E+ 成長計畫的主題文,會進一步談透過以特性為基礎的測試(Property-based testing) 方式,來確保測試的覆蓋率足夠完整。
閱讀更多
如果你對於「軟體測試」這主題感興趣,我們在 E+ 有寫更深入詳細的內容,包含測試金字塔 (Testing Pyramid)、測試金盃 (Testing Trophy)、整合測試、E2E 測試等主題。有興趣的讀者,歡迎加入 E+ 成長計畫。
本文為 E+ 成長計畫的深度內容,截取前三分之一開放免費閱讀。歡迎加入 E+ 成長計畫閱讀完整版本 (點此了解 E+ 的詳細介紹)