明明寫了大量測試程式碼,卻抓不到真正的錯誤,而且每修正一個功能,測試就接連失敗過嗎?
先說結論,優良的單元測試不是看數量或涵蓋率,而是看是否遵守「FIRST 原則」。FIRST 代表 Fast(快速)、Isolated(獨立)、Repeatable(可重複)、Self-validating(自我驗證)、Timely(適時)。
只要記住這五點,測試就不再是負擔,而會成為可靠的安全網。今天就搭配我的經驗,逐一說明。
FIRST 原則一覽
FIRST 是 Robert Martin(Uncle Bob)在《Clean Code》一書中介紹後廣為流傳的原則。
每個字母都代表優良單元測試的一項條件。
| 字母 | 意義 | 重點 |
|---|---|---|
| F | Fast | 數百個在幾秒內完成 |
| I | Isolated | 測試彼此不互相干擾 |
| R | Repeatable | 無論何時何地執行都得到相同結果 |
| S | Self-validating | 自動判定通過/失敗 |
| T | Timely | 在適當時機撰寫 |
接下來逐一拆解。
Fast:為什麼必須這麼快?
單元測試必須夠快。真的。
因為太慢,開發者就不會執行。一次要跑 3 分鐘的測試套件,最後往往只會在提交前勉強執行。
每次修改程式碼時,都能毫無負擔地執行數百次,測試才算發揮作用。
變慢最常見的原因是存取實際資料庫、網路和檔案系統。這些外部相依性應以測試替身(Mock、Stub)取代。
可以將目標訂為每個測試數毫秒,全部數百個測試在幾秒內完成。
Isolated:測試不能互相干擾的原因
每個測試都應該獨立,不能依賴其他測試的結果。
例如 A 測試建立的資料被 B 測試拿來使用時,A 一失敗,B 也會跟著崩潰。只要執行順序改變,結果也會不同。
因此,每個測試都應自行準備要使用的資料,結束後再確實清理。
以下是每次測試前初始化狀態的常見模式。
struct CalculatorTests {
let calculator: Calculator
init() {
// 每次測試都建立新執行個體 → 防止共用狀態 suite
calculator = Calculator()
}
}
如此一來,不論哪個測試先執行,都不會互相影響。
Repeatable & Self-validating:結果不能搖擺不定
先看 R,也就是可重複性。測試無論執行幾次、在哪個環境執行,都應得到相同結果。
今天通過、明天卻失敗的測試稱為「flaky 測試」,是破壞信任的主要元兇。
依賴目前時間或隨機值,很容易造成這類問題。若必須處理時間,注入固定值加以控制會更安全。
S,也就是自我驗證,同樣重要。測試必須自行判定通過或失敗。
把數值印到主控台,再由人用眼睛確認,並不算測試。應使用 #expect 等斷言自動判定結果。
優良測試只會顯示兩種結果:綠燈或紅燈。如果讓人思考「這樣對嗎?」,這個測試就已經失敗了。
Timely:什麼時候適合撰寫測試?
最後的 T 代表適時性,也就是在適當時機撰寫測試。
在 TDD(Test-Driven Development,測試導向開發)中,會先寫測試再寫正式程式碼。即使不完全採用這種方式,重點仍是在實作功能的前後一起撰寫。
如果功能完成後隔了幾週才想一次寫完測試,程式碼通常早已固定成難以測試的結構。
不要把測試拖到以後,這就是 Timely 的核心。
總結
FIRST 不是用來背誦的規則,而是一份檢查清單,當測試令人窒息時,幫你找出偏離的地方。
太慢嗎?彼此糾纏嗎?結果不穩定嗎?需要人工確認嗎?是不是太晚才寫?只要檢查這五點,測試品質就能大幅提升。
從今天開始,每寫一個測試時都稍微想起 FIRST。它一定會成為可靠的夥伴。

