哪些應該用單元測試驗證,哪些該留給整合測試 - 切界線的方法與實務判斷表
· 小村 豪 · 測試, 單元測試, 整合測試, 測試設計, Windows 開發, C# / .NET
測試設計的話題裡,每次都讓人暗自頭痛的,是要把多少東西塞進單元測試、從哪裡開始往整合測試搬。
這裡的兩個危險做法是:
- 想快就通通用單元測試
- 想接近真實就通通用整合測試
前者 mock 過多,會錯過正式環境才會炸的點;後者則讓測試又慢又易壞。
實務上要看的軸其實滿清楚:
- 想驗的是自家的邏輯,還是跟外部的串接?
- 換成 in-memory fake,意義會不會掉下來?
- DB / 檔案 / HTTP / DI / 設定 / 框架 / OS 的行為本身是不是重點?
- 要不要高速跑大量輸入組合?
這 4 題看清楚,單元測試與整合測試的界線就好畫多了。
本文以 2026 年 3 月時點可查閱的 Microsoft Learn 與 Martin Fowler 的公開資訊為前提,從實務角度整理兩者的界線。123
1. 先下結論
以粗但好用的說法:
- 純邏輯 → 單元測試
- 連接、接線、轉換、環境差異 → 整合測試
- 兩邊都能驗時,先選單元測試
- 整合測試不要做得又廣又重,要把邊界收窄
一句話:單元測試是「判斷的測試」,整合測試是「連接的測試」。
像金額計算、狀態轉移、輸入驗證、審核條件、例外分類這些「沒有外部資源也能有意義」的東西,放進單元測試,跑得快、壞得少、輸入組合也能鋪得厚。
而像 SQL 執行、JSON / CSV 序列化、路由、model binding、DI 註冊、檔案鎖、權限、COM 註冊、32bit / 64bit、STA / MTA 這些「一接上就可能背叛你的東西」,則應該放到整合測試。
Microsoft Learn 的 Integration tests in ASP.NET Core 也寫得很直白:整合測試要聚焦在關鍵基礎設施情境,能用單元測試處理的就用單元測試。
2. 本文所說的單元測試與整合測試
整理如下:
| 層級 | 要驗什麼 | 典型配置 |
|---|---|---|
| 單元測試 | 被隔離的單一職責是否正確 | 用 fake / mock / stub,切斷外部資源 |
| 整合測試 | 多個元件的連接,以及含基礎設施、框架的實際行為 | 實際 DB、實際檔案、實際 serializer、實際 host、實際 pipeline 等 |
| E2E/功能測試 | 整個應用的使用者流程 | 部署後的應用、多個服務、實際瀏覽器或實際行程 |
.NET 的單元測試說明把好的單元測試描述為 fast / isolated / repeatable,不應依賴檔案系統、資料庫這些外部因素。詳情可看 Unit testing best practices for .NET。
而整合測試並不是「一定要跨行程或跨機器的重型測試」。
就算在同一個行程內,只要 把多個實體元件串起來,並驗證框架或基礎設施的真實行為,那就偏整合測試。
例如在 ASP.NET Core 上對 controller action 做單元測試時,應該把對象收斂在 action 本體的判斷;routing、model binding、filters 這類與框架的互動放到整合測試 — 官方也這樣整理。詳情可參考 Unit test controller logic in ASP.NET Core。
3. 一張判斷表
先放最實務好用的表:
| 要驗什麼 | 主力測試 | 補充 |
|---|---|---|
| 金額計算、折扣、狀態轉移、輸入驗證 | 單元測試 | 輸入組合要鋪得厚 |
| 例外分類、錯誤訊息挑選、是否 retry 的判斷 | 單元測試 | 不需實際 I/O 就能驗完 |
| Repository 的 SQL / ORM 轉換、transaction | 整合測試 | 真實 DB 或 provider 的行為是主題 |
| JSON / XML / CSV 的 serialize / deserialize | 整合測試 | wire format 的偏差用 fake 難抓 |
| 路由、model binding、filter、middleware | 整合測試 | 驗證與框架的串接 |
| WPF / WinForms 的 ViewModel 或 Presenter 的狀態轉移 | 單元測試 | 不用起 UI 也有意義 |
| 真正的 Binding、Dispatcher、control lifecycle、message loop | 整合測試或 UI 測試 | 主題是框架與執行緒行為 |
| 檔案路徑、權限、鎖、共享資料夾、換行、編碼 | 整合測試 | 需要 OS 與檔案系統的真實行為 |
| COM 註冊、32bit / 64bit、STA / MTA、DLL 載入 | 整合測試 | 主題是環境差異與行程邊界 |
| 應用整體啟動、主要用例的通關確認 | E2E/冒煙 | 條數不用多 |
看的重點是:哪個測試最貼近「正式環境會壞的原因」。
用「程式碼放在哪」來決定會走偏,用「想消弭什麼不確定性」來決定比較穩。
4. 單元測試該接下什麼
單元測試適合 剝掉外部後仍有意義的職責。
例如:
- 業務規則
- 分支
- 狀態轉移
- 輸入驗證
- 錯誤分類
- 決定是否 retry
- ViewModel / Presenter 的狀態變化
- 轉換邏輯本身
特別是 組合越多的,越值得放進單元測試。
例如:
- 有/無優惠券
- 有/無庫存
- 首次/回購
- 管理員/一般使用者
- 正常值/邊界值/非法值
分支條件越多,整合測試要把它們全跑一遍會越重。
這裡放到單元測試細切比較合理。
另外,單元測試要 把外部因素變得可控:
- 目前時間用注入
- GUID、亂數要能被替換
- 不要靠 sleep 等
- 不碰真實 DB 或真實檔案
- 不走真實網路
這些守好,測試就會相當穩定。
4.1. 單元測試的 mock 變太多時
想寫單元測試,結果:
- 要 7 個 mock
- setup 很長
- arrange 比本體長
- 看不出到底在驗什麼
多半就是下列其一:
- 那個 class 職責過載
- 本該交給整合測試驗的接線,硬塞進單元測試
mock 的用途是把外部切開,不是用來證明「你和真實環境串得對」。
搞錯這點,就容易變成「全部 green 但正式環境掛掉」。
5. 該往整合測試的 4 個邊界
該往整合測試走的地方,大致可分 格式、接線、環境、時間 4 類。
5.1. 格式邊界
這裡說的格式包括:
- JSON / XML / CSV
- DB 的 schema 與 mapping
- nullable / precision / timezone
- enum、日期的序列化
- 編碼與 BOM
- 換行
Martin Fowler 也說,有 serialize / deserialize 的邊界都是整合測試的候選。詳細可看 The Practical Test Pyramid。
像:
- 把 DTO 轉 JSON 結果欄位名不對
- CSV 的引號、換行壞了
decimal被四捨五入掉- DB 上
DateTimeOffset處理偏了 null與空字串被當成一樣的東西
這類 bug 單元測試往往抓不到。
5.2. 接線邊界
接線邊界的例子:
- DI 註冊
- 設定 bind
- 路由
- model binding
- filter
- middleware
- host 啟動
- 事件接線
- WPF 的 Binding 或 command 綁定
這些的重點不在「我的函式對不對」,而是 多個實體元件有沒有被正確接起來。
在 ASP.NET Core,官方把 controller action 的單元測試收斂到 action 的判斷本身;routing、model binding、filters 留給整合測試。
桌面應用也是同樣思路:ViewModel 的狀態轉移用單元測試,包含實際 XAML Binding、Dispatcher 的行為則偏整合測試。
5.3. 環境邊界
在 Windows 開發裡,這塊特別重要:
- 檔案權限
- 共享資料夾
- 檔案鎖
- 從暫存檔 rename
- 系統管理員權限
- 服務啟動權限
- COM 註冊
- 32bit / 64bit
- STA / MTA
- DLL 從哪載入
這些都是 OS 或執行環境本身的條件 當主角。
用 in-memory fake 會失真太多,應該交給整合測試。
特別是既有 Windows 軟體或含 COM / ActiveX 的架構,常常不是邏輯出事,而是 註冊、bitness、執行緒模型、權限 先出事。
這類故障不是單元測試的守備範圍,而是要把環境納入的整合測試才撿得到。
5.4. 時間邊界
另一個容易漏的是時間與並行。
- timeout
- cancellation
- retry 的真實行為
- timer 驅動
- 背景處理的停止
- race condition
- shutdown 時的結束順序
這裡重點是 把「判斷」與「真實行為」分開。
例如:
- 最多 retry 幾次
- 哪些例外要 retry
用單元測試就夠。
但:
- timeout 真的有生效嗎
- cancellation 有傳遞下去嗎
- timer 和非同步處理撞在一起會不會壞
- 結束時 handle、task 是否乾淨關閉
就偏整合測試了。
6. 常見判斷錯誤
6.1. 把 Repository 全 mock 了就滿足
把 Repository 相關都 mock 掉,下面這些都驗不到:
- SQL 對不對
- transaction 有沒有效
- 與 schema 是否一致
- mapping 有沒有歪
- 編碼或 precision 有沒有壞
Repository 往往不是 邏輯測試的對象,而是 邊界的接點。
這種情況應把整合測試的比重拉高,比單元測試更貼近實況。
6.2. 在 Controller / Endpoint 的單元測試想連框架一起驗
controller action 的單元測試該驗的是:
- 條件分支
- 回傳值的選擇
- 相依服務的呼叫方式
而:
- route 對不對得上
- model binding 能不能通
- filter 生效了嗎
- 過完 middleware 後變怎樣
是整合測試的事。
混在一起就分不清哪裡壞掉。
6.3. 用整合測試窮舉輸入組合
整合測試接近真實,所以一定慢。
所以 分支窮舉給單元測試,邊界代表情境給整合測試 比較划算。
Microsoft Learn 的整合測試說明也推薦:對 DB 或檔案系統不要用整合測試跑所有組合,而是 收斂到 read / write / update / delete 這類代表性情境。
6.4. 在 CI 直接打外部服務的正式環境
這應該避免。
整合測試強調「接近真實」,但不代表要天天打真正的 SaaS 或正式 API。
Fowler 也建議把外部服務搬到本地、用 fake,或用專用的 test instance。
實務上常用的組合是:
- 本地 DB
- 暫存資料夾
- test host
- 專屬的 test environment
- 以契約為準的 fake service
7. 實務上的建議架構
比例沒有絕對答案,但下面這 3 層非常通用:
| 層 | 主力 | 放什麼 |
|---|---|---|
| 核心層 | 單元測試做厚 | 業務規則、狀態轉移、輸入驗證、錯誤分類 |
| 邊界層 | 窄而精的整合測試 | DB、檔案、HTTP、serializer、DI、設定、COM、權限 |
| 整體層 | 少量的冒煙 / E2E | 啟動確認、主要流程、重大故障的防再發 |
感覺上,用數量撐起來的是單元測試,用邊界濃度撐起來的是整合測試。
推薦做法:
- 先盤點應用的邊界
- 把邏輯重整成「可和外部切開」的形狀
- 每個邊界至少放 1 條 happy path + 1 條代表性 failure path
- 整體通關的條數要收斂
- 出 bug 時,把測試補在 能用最低成本重現那個 bug 的那一層
第 5 點很關鍵:
- 規則寫錯:加單元測試
- SQL/binding/設定/權限/註冊錯:加整合測試
- 包含啟動或部署的故障:加冒煙或 E2E
這樣擴張,測試的職責不會走樣。
8. 猶豫時最後問的 5 個問題
猶豫時把這 5 題走一遍:
- 換成 in-memory fake,要驗的意義還在嗎
- 還在 → 偏單元測試
- 壞掉時首先懷疑的是邏輯,還是連接、設定
- 連接或設定 → 偏整合測試
- 主題是 DB/檔案/serializer/DI/route/model binding/OS/權限/bitness/thread 嗎
- 是 → 偏整合測試
- 要不要高速跑大量輸入組合
- 要 → 偏單元測試
- 這個測試壞了,能馬上知道該改哪裡嗎
- 不能 → 你的測試層級混在一起了
用這 5 題,就比較不會用「感覺比較接近真實,放整合測試」「感覺比較快,放單元測試」這種粗放決策。
9. 總結
單元測試與整合測試的界線,不該用「程式碼放哪」來切,而該用 要消弭什麼不確定性 來切,這才最實務。
重點整理:
- 單元測試是判斷的測試
- 整合測試是連接的測試
- 分支窮舉走單元測試
- 格式、接線、環境、時間走整合測試
- 整體通關用少量的冒煙 / E2E 押住
最該避免的是:
- 以為 mock 也能證明「和真實的連接是對的」
- 整合測試去跑所有分支
- 把單元測試與整合測試的職責混在一起
猶豫時先問:這個 bug 是「判斷」壞,還是「連接」壞?
光這一題,多數情境就能劃出清楚界線。
10. 相關文章
11. 參考資料
-
Microsoft Learn, Integration tests in ASP.NET Core ↩
-
Microsoft Learn, Unit testing best practices for .NET ↩
-
Martin Fowler, The Practical Test Pyramid ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
無法避免自行實作 logger 時,真正必要的最小要件是什麼:實務要件與整合測試觀點
本文整理當無法使用既成 logging framework、必須自行實作應用程式日誌時,第一版該守住的最小要件,包含 UTF-8 JSON Lines、必要欄位、一個行程一個檔案、flush 條件、輪轉與保留,並列出以真實檔案、執行緒、行程驗證的整合測試清單,協助讀者打造在...
用 Windows 沙箱加速 Windows 應用程式開發的驗證 - 以實務向整理管理員權限問題、乾淨環境、權限不足・資源不足的重現
整理在 Windows 應用程式開發中如何運用 Windows Sandbox 加速驗證的實務做法。透過按情境分檔的 .wsb、唯讀輸入與專用 Outbox 寫入分離、在容器內另建標準使用者重現權限不足、以及降低記憶體和關閉 vGPU 製造資源不足偏向,把每次的乾淨環境準備...
發生非預期例外時的 checklist - 要讓應用結束還是繼續,先看的判斷表
本文以 C# / .NET 與 Windows 應用為前提,把非預期例外發生後該結束還是繼續的判斷拆成失敗單位、共用狀態、外部副作用、原生邊界四個觀察點,並提供判斷表與典型情境,協助讀者在 catch 之前先判斷是否還能信任應用狀態。
Windows 應用程式中把「僅需要系統管理員權限的處理」分離出來的具體寫法
本文以 .NET 8 桌面應用程式為例,具體展示如何讓 UI 保持 asInvoker,把僅需系統管理員權限的處理切到 helper EXE。涵蓋 manifest、runas 啟動、具名管道 ACL、PID 驗證、固定 operation 與請求驗證,以及 Explore...
Windows 應用程式不要把機密資訊以明文存進設定檔的最佳實踐
本文整理 Windows 桌面應用程式儲存連線憑證或 API Token 時的實務做法,說明為何 DPAPI 與 ProtectedData 比明文或自做加密更能切斷檔案外流即等同機密外流的鏈條,並比較 CurrentUser 與 LocalMachine 的適用情境、優先...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。