QuickCheck 是一個用於隨機測試的 Haskell 工具庫,本文將基於原書中的案例以及函數式編程方法討論如何構建 Swift 版本的 QuickCheck 庫。javascript
注:在學習本章內容之前,筆者沒有學習過 Haskell,也沒有使用過 QuickCheck,本文是經過原書及一些網絡資料學習後的心得,若有錯誤或遺漏,歡迎批評指正。java
QuickCheck 項目始於 1999 年,做者 Koen Claessen 和 John Hughes 在其論文《QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs》中對測試工具應該具有的特性進行了討論,主要有如下兩點:git
這兩點仍是比較容易理解的,首先,測試人員應該提供一個可以讓測試工具自動化判斷用例是否成功的標準,而後,測試工具應該可以基於該標準自動化生成測試用例,以便於對應用程序進行隨機測試。程序員
此外,做者還提到 QuickCheck 的一個重要設計思想:"An important design goal was that QuickCheck should be lightweight."github
下面來具體瞭解一下 QuickCheck,維基百科中描述以下:編程
QuickCheck is a combinator library originally written in Haskell, designed to assist in software testing by generating test cases for test suites. It is compatible with the GHC compiler and the Hugs interpreter.swift
In QuickCheck the programmer writes assertions about logical properties that a function should fulfill. Then QuickCheck attempts to generate a test case that falsifies these assertions. Once such a test case is found, QuickCheck tries to reduce it to a minimal failing subset by removing or simplifying input data that are not needed to make the test fail.數組
QuickCheck 最初是基於 Haskell 實現的一個庫,主要目標是經過生成用例來輔助軟件測試。其主要功能邏輯是:網絡
因而可知,一個 QuickCheck 應該包含如下 4 個組成部分:dom
構建 Swift 版本的 QuickCheck,咱們須要作的也就是構建以上 4 個組成部分。
這裏的隨機數並不特指數值類型,而是應該支持諸如字符、字符串等「各類各樣」的類型,爲此,咱們能夠定義一個生成隨機數的協議:
protocol Arbitrary {
static func arbitrary() -> Self
}複製代碼
這樣,想要生成哪一種類型的隨機數,只須要遵循該協議,並實現對應 arbitrary()
便可。以 Int
爲例:
extension Int: Arbitrary {
static func arbitrary() -> Int {
return Int(arc4random())
}
}
print(Int.arbitrary()) // "3212540033"複製代碼
用例成功/失敗驗證標準,即一個函數應該知足的邏輯屬性(property
),也就是 QuickCheck 做者所說的 "determine whether a test is passed or failed"。所以,property
的定義應該形如:
typealias property = A: Arbitrary -> Bool // 語法錯誤,這裏僅作示意複製代碼
使用 property
來對輸入的隨機數進行驗證,成功返回 true
,失敗返回 false
,QuickCheck 經過重複生成隨機數並驗證,來尋找某一個使驗證失敗的用例,爲此咱們還須要一個 check
函數:
let numberOfIterations = 100
func check<A: Arbitrary>(_ property: (A) -> Bool) -> Void {
for _ in 0 ..< numberOfIterations {
let value = A.arbitrary()
guard property(value) else {
print("Failed Case: \(value).")
return
}
}
print("All cases passed!")
}複製代碼
check
函數的主要功能有:
for _ in 0 ..< numberOfIterations
);let value = A.arbitrary()
);guard property(value)
)。完成了前兩步工做以後,咱們還須要將失敗用例的範圍儘可能縮小,以便咱們更容易的定位問題代碼。所以咱們但願輸入的隨機數可以縮減,並從新運行驗證過程。
爲此,咱們能夠定義一個 Smaller
協議來對輸入進行縮減處理(原書作法),一樣的,咱們還能夠擴展隨機數協議(protocol Arbitrary
),爲其添加縮減方法。這裏咱們採用後一種:
protocol Arbitrary {
static func arbitrary() -> Self
static func shrink(_ : Self) -> Self?
}複製代碼
shrink
函數可以對輸入的隨機數進行縮減並返回,不過,返回值咱們使用了可選類型,也就是說,一些輸入是沒法再被縮減的,例如空數組,這時咱們須要返回 nil
。
下面,咱們修改以上 Int
擴展,爲其添加 shrink
函數:
extension Int: Arbitrary {
static func arbitrary() -> Int {
return Int(arc4random())
}
static func shrink(_ input: Int) -> Int? {
return input == 0 ? nil : input / 2
}
}
print(Int.shrink(100)) // Optional(50)複製代碼
在上述例子中,對於整數,咱們嘗試使用除以 2 的方式來進行縮減,直到等於零。
事實上,用例縮減是一個反覆的過程,甚至多是一個「無限」的過程,所以,咱們將這個「無限」縮減的過程使用函數來代替:
func iterateWhile<A: Arbitrary>(condition: (A) -> Bool, initial: A, next: (A) -> A?) -> A {
if let x = next(initial), condition(x) {
return iterateWhile(condition: condition, initial: x, next: next)
}
return initial
}複製代碼
咱們能夠在發現失敗用例時,經過調用 iterateWhile
函數來縮減輸入用例,這樣,咱們就能夠進一步改造 check
函數了:
func check_2<A: Arbitrary>(_ property: (A) -> Bool) -> Void {
for _ in 0 ..< numberOfIterations {
let value = A.arbitrary()
guard property(value) else {
// 縮減用例
let smallerValue = iterateWhile({ !property($0) }, initial: value) {
A.shrink($0)
}
print("Failed Case: \(smallerValue).")
return
}
}
print("All cases passed!")
}複製代碼
在測試結果輸出這一步,咱們沒有作更多的事情,只是簡單的輸出結果,這裏再也不贅述。
QuickCheck 可以幫助咱們快速對函數功能進行測試,並經過用例縮減方式協助定位代碼中的問題,使用 QuickCheck 測試驅動開發,還可以迫使咱們思考函數所承擔的職責以及須要知足的抽象特性,幫助咱們設計、開發出模塊化、低耦合的程序。
在理解了 QuickCheck 的思想以後,咱們構建了簡單的 Swift 版本 QuickCheck,其中融入了函數式思想,咱們將整個問題分解爲 4 個部分,並分別編寫了隨機數生成函數、用例驗證函數、用例縮減函數以及將這幾部分組合起來的 check
函數,從而完成了 QuickCheck 功能。不過距離可以投入使用還有很大的差距。
目前,已經有開發者完成了一套較爲完善的 Swift 版 QuickCheck,名爲 SwiftCheck,須要實際應用或是進一步學習能夠查閱。
本文屬於《函數式 Swift》讀書筆記系列,同步更新於 huizhao.win,歡迎關注!