軟體測試 — 如何寫出好維護的測試?

2025年7月23日

💎 加入 E+ 成長計畫 如果你喜歡我們的內容,歡迎加入 E+,獲得更多深入的軟體前後端內容

先前我們談了測試金字塔的概念 ,在這一期我們將延續這個主題,進一步來談如何寫出好測試的程式碼,以及可以如何讓測試更好維護。

如何讓測試好維護

在談完如何寫出好測試的程式碼,接著我們來談如何讓測試比較好維護。

測試的意圖

首先,要讓測試好維護,要盡可能避免測試的意圖不明,讓人讀不懂該測試到底在測什麼。在業界中有個觀點是,好的測試就像文件一樣,讓人可以透過測試來了解程式碼庫。從這角度來看,當測試無法讓人輕易看懂,該測試就相對沒有價值。

在寫測試時,有一個叫 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+ 的詳細介紹)

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們