測試與程式碼品質

優良單元測試的條件:FIRST 原則 5 大重點總整理

明明寫了大量測試程式碼,卻抓不到真正的錯誤,而且每修正一個功能,測試就接連失敗過嗎?

閱讀 4 分鐘
優良單元測試的條件:FIRST 原則 5 大重點總整理 封面圖

明明寫了大量測試程式碼,卻抓不到真正的錯誤,而且每修正一個功能,測試就接連失敗過嗎?

先說結論,優良的單元測試不是看數量或涵蓋率,而是看是否遵守「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()
    }
}

如此一來,不論哪個測試先執行,都不會互相影響。

在 Xcode 中使用 @Test 與 init() 撰寫的 Swift Testing 測試套件,以及測試通過結果畫面
有了 init(),每個測試都會從新的執行個體開始,彼此保持獨立

Repeatable & Self-validating:結果不能搖擺不定

先看 R,也就是可重複性。測試無論執行幾次、在哪個環境執行,都應得到相同結果。

今天通過、明天卻失敗的測試稱為「flaky 測試」,是破壞信任的主要元兇。

依賴目前時間或隨機值,很容易造成這類問題。若必須處理時間,注入固定值加以控制會更安全。

S,也就是自我驗證,同樣重要。測試必須自行判定通過或失敗。

把數值印到主控台,再由人用眼睛確認,並不算測試。應使用 #expect 等斷言自動判定結果。

優良測試只會顯示兩種結果:綠燈或紅燈。如果讓人思考「這樣對嗎?」,這個測試就已經失敗了。


Timely:什麼時候適合撰寫測試?

最後的 T 代表適時性,也就是在適當時機撰寫測試。

在 TDD(Test-Driven Development,測試導向開發)中,會先寫測試再寫正式程式碼。即使不完全採用這種方式,重點仍是在實作功能的前後一起撰寫。

如果功能完成後隔了幾週才想一次寫完測試,程式碼通常早已固定成難以測試的結構。

不要把測試拖到以後,這就是 Timely 的核心。


總結

FIRST 不是用來背誦的規則,而是一份檢查清單,當測試令人窒息時,幫你找出偏離的地方。

太慢嗎?彼此糾纏嗎?結果不穩定嗎?需要人工確認嗎?是不是太晚才寫?只要檢查這五點,測試品質就能大幅提升。

從今天開始,每寫一個測試時都稍微想起 FIRST。它一定會成為可靠的夥伴。

延伸閱讀