當你編碼時,目標是使程序正常工作。 但作為測試設計者,你希望讓它失敗。 這是一個微妙但重要的區別。
為什么軟件測試很難?
- 做不到十分詳盡:測試一個 32 位浮點乘法運算 。有 2^64 個測試用例!
- 隨機或統計測試效果差:其他工程學科可以測試小的隨機樣本(例如制造的硬盤驅動器的 1%)并推斷整個生產批次的缺陷率。但是對于軟件來說并非如此。軟件行為在可能輸入的空間內不連續且離散地變化,系統可能在廣泛的輸入范圍內似乎工作正常,然后在單個邊界點突然失效,堆棧溢出、內存不足錯誤和數字溢出錯誤往往會突然發生。
Test-first programming
順序:
- Specification (規范):編寫函數簽名和注釋,明確其行為、輸入約束和輸出。
- Test (測試):根據規范編寫測試用例。
- Implementation (實現):編寫實現代碼,并通過已寫的測試。
測試優先編程的最大好處是防止錯誤。 不要將測試留到開發結束,因為此時有一大堆未經驗證的代碼。 將測試留到最后只會使調試時間更長、更痛苦,因為錯誤可能存在于代碼中的任何位置。
Systematic testing
我們希望進行系統測試,而不是詳盡、隨意或隨機的測試。系統測試意味著我們以有原則的方式選擇測試用例,目標是設計一個具有三個理想屬性的測試套件:
通過分區選擇測試用例
我們希望選擇一組足夠小的測試用例,以便于編寫和維護并快速運行,但又足夠徹底以發現程序中的錯誤。
為此,我們將程序的輸入空間劃分為多個子域,每個子域由一組輸入組成。我們只需要為每個集合測試一個代表。 這種方法通過選擇不同的測試用例,并強制測試探索隨意或隨機測試可能無法到達的輸入空間區域,從而充分利用了有限的測試資源。
例:
Math.abs()
測試用例:
- a = 17 覆蓋子域 a > 0
- a = 0 覆蓋子域 a = 0
- a = -3 覆蓋子域 a < 0
Math.max()
測試用例:
- (a,b) = (1, 2) 覆蓋 a < b
- (a,b) = (10, -8) 覆蓋 a > b
- (a,b) = (9, 9) 覆蓋 a = b
子域應具有三個理想的屬性:
- 互斥
- 完整
- 非空
自動化單元測試
- 單元測試 (Unit Test):測試單個模塊(如函數)的測試。
- 自動化:使用測試框架(如Mocha for JS/TS)編寫測試代碼,自動運行并檢查結果(使用
assert.strictEqual
,assert.deepStrictEqual
等斷言),輸出通過/失敗報告。 - 文檔化測試策略 (Documenting Strategy):在測試代碼中以注釋形式記錄所采用的分區策略,并為每個測試用例命名其所覆蓋的子域(如
it("covers a < b", ...)
),這極大地增強了測試套件的可理解性。
黑盒 vs 玻璃盒測試
- 黑盒測試:僅根據規范選擇測試用例,不查看實現代碼。這是測試優先編程的天然方式。
- 玻璃盒測試:基于對實現代碼的了解選擇測試用例(例如,測試不同的算法分支、內部緩存機制等)。
- 結合使用:先進行黑盒測試(定義分區),再通過玻璃盒測試和覆蓋率分析來補充測試用例,提高徹底性。
覆蓋率
衡量測試套件對代碼的覆蓋程度,常用指標:
- Statement coverage (語句覆蓋):是否每條語句都被至少一個測試執行過?(常見目標)
- Branch coverage (分支覆蓋):是否每個控制分支(如if/else的兩邊)都被至少一個測試執行過?
- Path coverage (路徑覆蓋):是否所有可能的執行路徑都被覆蓋?使用工具(如Istanbul/nyc)測量覆蓋率,并補充測試用例以提高覆蓋率。
單元測試 vs. 集成測試
- 單元測試:孤立地測試單個模塊。優點:錯誤更容易定位(就在被測試的模塊中)。
- 集成測試:測試多個模塊的組合或整個系統。必要但錯誤可能出現在任何連接的模塊中。
- 策略:首先依靠全面的單元測試建立對各個模塊的信心,然后使用集成測試來檢查模塊間的交互。盡量避免在單元測試中依賴其他可能出錯的模塊。
自動化回歸測試
- 回歸測試 (Regression Testing):在每次修改代碼后(修復bug、添加功能、優化性能)運行完整的測試套件,防止修改引入新的錯誤。
- 測試優先調試 :發現bug時,立即編寫一個能重現該bug的測試用例,并將其加入測試套件。修復bug后,該測試用例就成為防止未來回歸的回歸測試。
- 自動化是回歸測試可行的關鍵。
迭代式測試優先編程
軟件開發不是線性的,應采用迭代方式:
- 編寫初步規范和測試。
- 編寫初步實現。
- 根據實現中發現的問題,迭代改進規范、測試和實現。
迭代允許更快地獲得反饋,更有效地利用時間,特別是在解決復雜問題時。