【函數式 Swift】QuickCheck

QuickCheck 是一個用於隨機測試的 Haskell 工具庫,本文將基於原書中的案例以及函數式編程方法討論如何構建 Swift 版本的 QuickCheck 庫。javascript

注:在學習本章內容之前,筆者沒有學習過 Haskell,也沒有使用過 QuickCheck,本文是經過原書及一些網絡資料學習後的心得,若有錯誤或遺漏,歡迎批評指正。java


QuickCheck 概述

QuickCheck 項目始於 1999 年,做者 Koen Claessen 和 John Hughes 在其論文《QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs》中對測試工具應該具有的特性進行了討論,主要有如下兩點:git

  1. "A testing tool must be able to determine whether a test is passed or failed; the human tester must supply an automatically checkable criterion of doing so."
  2. "A testing tool must also be able to generate test cases automatically."

這兩點仍是比較容易理解的,首先,測試人員應該提供一個可以讓測試工具自動化判斷用例是否成功的標準,而後,測試工具應該可以基於該標準自動化生成測試用例,以便於對應用程序進行隨機測試。程序員

此外,做者還提到 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 實現的一個庫,主要目標是經過生成用例來輔助軟件測試。其主要功能邏輯是:網絡

  1. 首先,程序員使用 QuickCheck 編寫斷言,用於驗證某個函數是否知足其邏輯特性;
  2. 而後,QuickCheck 將隨機生成測試用例來使上述斷言失敗;
  3. 接着,一旦發現失敗的用例,QuickCheck 會盡量減小失敗用例的輸入值,以便於快速定位問題所在;
  4. 最後,輸出測試結果:成功,或是會致使測試失敗的最小用例集合。

因而可知,一個 QuickCheck 應該包含如下 4 個組成部分:dom

  1. 隨機數生成;
  2. 用例成功/失敗驗證標準;
  3. 用例範圍最小化;
  4. 測試結果輸出。

構建 Swift 版本 QuickCheck

構建 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 函數的主要功能有:

  1. 控制循環次數(for _ in 0 ..< numberOfIterations);
  2. 建立隨機輸入(let value = A.arbitrary());
  3. 驗證用例是否成功(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,須要實際應用或是進一步學習能夠查閱。

參考資料

  1. Github: objcio/functional-swift
  2. Wikipedia: Haskell
  3. Wikipedia: QuickCheck
  4. Introduction to QuickCheck1
  5. QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs
  6. Github: typelift/SwiftCheck

本文屬於《函數式 Swift》讀書筆記系列,同步更新於 huizhao.win,歡迎關注!

相關文章
相關標籤/搜索