Доверяй, но проверяй (Trust, but verify)
— 俄羅斯諺語
核心概念#
我們建議為函式撰寫單元測試,基於你對被測事物的了解來思考可能出問題的典型場景。但如果你自己寫程式碼又自己寫測試,是否可能在兩者中都表達了同一個錯誤的假設?程式碼通過測試,因為它做了基於你的理解的事——但你的理解可能是錯的。
與其讓不同的人寫測試(這會失去「思考測試如何影響程式碼」的好處),不如讓電腦幫你做一些測試——電腦不分享你的先入之見。
合約、不變量與屬性#
在 Topic 23(契約式設計)中,我們談到程式碼有合約(contracts):你滿足輸入條件,它保證輸出的特性。也有不變量(invariants):通過函式後某些狀態保持不變。例如排序一個列表,結果的元素數量與原始列表相同——長度是不變量。
把合約和不變量合稱為屬性(properties),我們可以用它們來自動化測試——這就是以屬性為基礎的測試(property-based testing)。
Tip 71 - Use Property-Based Tests to Validate Your Assumptions(使用以屬性為基礎的測試來驗證你的假設)
排序範例#
對排序列表可以建立兩個屬性測試:
- 排序後的列表長度與原始列表相同(不變量)
- 結果中任何元素都不大於其後的元素(後置條件)
使用 Python 的 Hypothesis 框架,每個屬性測試會自動用 100 個不同的隨機列表運行——等於不費力就產生了 200 個測試。
找出錯誤的假設——倉庫範例#
一個簡單的訂單處理和庫存控制系統,有 Warehouse 物件可以查詢庫存、取出商品。基本的單元測試全部通過。但加入屬性測試後——驗證「取出數量加上剩餘庫存應等於初始庫存」——發現了一個 bug:
in_stock 函式只檢查至少有一個商品,沒有檢查是否有足夠的商品。嘗試取出 3 頂帽子(但只有 2 頂庫存),系統拋出「Oversold」例外。
修復方法是讓 in_stock 同時接受 quantity 參數,檢查庫存是否足夠。
屬性測試找到的 bug 往往不是你預期的那種。在上述範例中,測試本意是驗證庫存調整是否正確,但實際上找到了
in_stock函式的 bug。這既是屬性測試的力量,也是它的挫折所在——你永遠不完全知道會發生什麼。
屬性測試經常帶來驚喜#
當屬性測試失敗時,找出傳遞給測試函式的參數,然後用這些值建立一個獨立的常規單元測試。這有兩個好處:
- 讓你聚焦問題本身
- 成為一個回歸測試——因為屬性測試使用隨機值,無法保證下次使用相同值
屬性測試也改善你的設計#
屬性測試讓你用不變量和合約來思考程式碼——思考什麼不能改變、什麼必須為真。這種額外的洞察力對程式碼有神奇的效果:去除邊界案例、突顯留下不一致資料的函式。
屬性測試和單元測試是互補的——它們處理不同的關注點,各自帶來獨特的好處。
相關章節#
- Topic 23,契約式設計
- Topic 25,斷言式程式設計
- Topic 45,需求之坑