[工程觀念] 單元測試要測什麼、不要測什麼:釐清邊界,提高測試效益!
前言
欸,各位工程師朋友好啊!今天 Hikari 我要來跟大家聊一個,在開發流程中很常聽到,但又常常讓人「不知道怎麼寫」、「寫了覺得沒用」的議題:**單元測試 (Unit Test)**!
很多人對單元測試可能抱持著各種想法:
- 「我們專案沒時間寫單元測試啦!」
- 「寫了單元測試,結果每次改程式碼都要改測試,超煩的!」
- 「不知道要測什麼,就隨便測一下,應付 CI/CD 而已。」
這些心聲我完全能理解!單元測試的確是個雙面刃,寫得好能讓你信心滿滿、快速迭代;寫不好則變成沈重的負擔,每次重構都像在拆地雷。其實,問題的核心往往在於我們對「單元測試的邊界」不夠清晰,搞不清楚它到底該測什麼、不該測什麼。
今天,我們就來好好聊聊,一個有效的單元測試,到底應該關注哪些點?又該避開哪些坑? 希望能幫助大家更有效率地運用單元測試,讓它成為你開發路上的好幫手,而不是絆腳石!
單元測試,到底要測什麼?
首先,我們得搞清楚「單元」是什麼?在大多數情況下,一個「單元」指的是你程式碼中最小的、可獨立測試的邏輯片段,通常是一個函式 (Function) 或一個方法 (Method)。重點在於「單一職責 (Single Responsibility)」和「隔離 (Isolation)」。
釐清了「單元」後,我們可以把單元測試的目標聚焦在以下幾個方面:
1. 核心業務邏輯的正確性
這是單元測試最主要的目標。任何重要的商業規則、計算邏輯、演算法,都應該透過單元測試來驗證其正確性。例如:
- 計算邏輯:折扣計算、稅金計算、分數排名。
- 資料轉換:輸入字串解析成物件、日期格式轉換。
- 條件判斷:使用者權限檢查、訂單狀態判斷、商品庫存檢查。
舉個 C# 的例子:
1 | public class OrderService |
這個 CalculateTotalPrice 方法的單元測試,就應該驗證不同單價、數量、折扣率組合下的總價是否正確。
2. 邊界條件 (Edge Cases)
程式碼最容易出錯的地方之一就是邊界條件。你的函式對於「極端值」、「無效輸入」或「空值」的處理是否正確?例如:
- 空值/空集合:輸入
null或空字串""、空列表[]。 - 零或負數:數量為零、價格為負數。
- 最大/最小值:整數的最大值、字串的最大長度。
延續上面的例子,我們應該測試 quantity 為 0 或負數時,是否會拋出預期的例外;discountRate 超出 0 到 1 範圍時,是否也處理得當。
3. 錯誤處理流程 (Error Handling)
當你的程式碼預期會發生某些錯誤時,單元測試應該確保這些錯誤被正確地捕捉、處理或拋出。這包括:
- 無效輸入:輸入不符合預期格式時,是否拋出
FormatException或ArgumentException。 - 依賴失敗:模擬內部依賴發生錯誤時,你的單元是否能正確回應。
4. 回歸測試的基礎
單元測試提供了最強大的回歸保護網。當你修改了程式碼,或是重構了一段邏輯時,執行現有的單元測試可以立刻告訴你,你的改動是否意外地破壞了原有的功能。這能極大地增加你對程式碼變更的信心。
單元測試,什麼「不」該測?
理解了該測什麼之後,同樣重要的是知道什麼不該測。把精力浪費在不該測的東西上,只會讓你的測試變得又慢又脆弱,最終成為負擔。
1. 外部依賴 (External Dependencies)
這是單元測試最大的陷阱! 外部依賴包括:
- **資料庫 (Database)**:SQL Server, MySQL, MongoDB 等。
- **網路請求 (Network Calls)**:呼叫外部 API、微服務。
- **檔案系統 (File System)**:讀寫檔案。
- **時間日期 (Current Date/Time)**:
DateTime.Now或Date.now()。
為什麼不該測?
- 不穩定:資料庫可能連線失敗、網路可能延遲或斷線、檔案可能不存在。
- 太慢:這些操作通常很耗時,會拖慢測試執行速度,導致開發者不愛跑測試。
- 難以隔離:依賴外部資源會讓測試結果受到環境影響,無法保證在任何地方執行都一致。
那怎麼辦?
使用 Mocking (模擬) 或 **Stubbing (替身)**。簡單來說,就是用一個假的、可控制的物件來替換掉真實的外部依賴。這樣你的單元測試就只關注你自己的程式碼邏輯,而不用擔心外部服務的狀態。
1 | // 假設有一個介面定義了外部資料存取 |
上面這個例子,我們透過 Mock<IUserRepository>() 建立了一個假的 IUserRepository。當 GetUserById(1) 被呼叫時,它會直接回傳一個我們預設好的 User 物件,而不是真的去查資料庫。這樣測試就非常快速且穩定。
2. 框架或函式庫本身
你所使用的程式語言、框架 (例如 ASP.NET Core, Spring Boot, React) 或是第三方函式庫 (例如 Lodash, Moment.js),它們通常都有自己嚴謹的測試。你不需要重新測試這些已經被驗證過的東西。你的重點應該放在你用這些工具組合起來的「獨特邏輯」。
例如,你不需要測試 List<T>.Add() 方法是否能成功加入元素,因為那是 .NET 框架的責任。你需要測試的是你利用 List<T> 實現的某個特定業務邏輯。
3. 程式碼的實作細節 (Implementation Details)
這是一個很常見的誤區。單元測試應該測試**單元的行為 (Behavior),而不是它的內部實作 (Implementation)**。不要測試私有方法、內部狀態,除非這些私有方法或內部狀態是達成公開行為的必要環節。
為什麼不該測?
- 脆弱的測試:當你重構程式碼,改變了內部實作(例如把一個方法拆成兩個私有方法,或改變變數名稱),但其公開行為不變時,如果你的測試是針對實作細節的,這些測試就會「壞掉」。這會導致你不敢重構,或是重構的成本變高。
- 失去彈性:測試綁定實作細節,會讓程式碼很難修改。
專注於「當我給這個函式 A 輸入,它應該輸出 B」,而不是「這個函式會先呼叫私有方法 X,然後再呼叫私有方法 Y」。
4. 瑣碎的 Getter/Setter 或純粹的資料傳遞
如果你的類別只是簡單的資料容器 (POCO / DTO),僅包含 get 和 set 屬性,沒有任何業務邏輯,那為這些屬性寫單元測試的投資報酬率通常很低。它們幾乎不可能出錯。把時間花在測試那些真正有邏輯判斷、計算和複雜流程的程式碼上會更有價值。
講到這,什麼才是好的單元測試?
一個好的單元測試,通常會遵循 F.I.R.S.T 原則:
- **Fast (快速)**:測試應該跑得很快,這樣開發者才會願意頻繁執行。
- **Independent (獨立)**:每個測試案例都應該獨立於其他測試,執行順序不影響結果。
- **Repeatable (可重複)**:在任何環境下重複執行,結果都應該一致。
- **Self-Validating (自我驗證)**:測試結果應該是明確的 Pass 或 Fail,不需要人工判斷。
- **Timely (及時)**:在寫程式碼之前或之後「立刻」寫,以確保測試與程式碼同步。
同時,要記得:關注行為,而非實作。你的單元測試應該像黑箱一樣,只關心輸入和輸出,而不去窺探盒子裡面的運作方式。這會讓你的測試更堅固,更能承受重構。
小結
單元測試是個強大的工具,但它終究是工具,不是目的。寫單元測試的目的,是為了提高程式碼品質、增加開發信心、降低 Bug 機率,並且加速迭代。
釐清「單元」的邊界,知道哪些該測、哪些不該測,是寫出高效單元測試的關鍵。把時間花在測試核心邏輯、邊界條件和錯誤處理上,並利用 Mocking/Stubbing 來隔離外部依賴,你會發現單元測試不再是負擔,而是你開發流程中不可或缺的加速器。
希望這篇文章能幫助大家對單元測試有更清晰的認識!如果大家有什麼單元測試的實戰心得或踩雷經驗,也歡迎在底下留言跟我分享喔!我們下次見!
如果您喜歡我寫的文章,幫我按個5下讚吧!感謝您的鼓勵和支持!

![[工程觀念] 單元測試要測什麼、不要測什麼:釐清邊界,提高測試效益!](/img/covers/unit-test-what-to-test-what-not-to-test.jpg)
![[AI] 提示詞注入(Prompt Injection)與防禦:讓你的 LLM 應用不再被綁架!](/img/covers/prompt-injection-defense.jpg)
![[AI] LLM 的 Function Calling / Tool Use 怎麼運作?白話拆解原理與實戰眉角](/img/covers/llm-function-calling-tool-use-how-it-works.jpg)