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,欢迎关注!