Swift 的 QuickCheck。
对于那些已经熟悉 Haskell 库的人,请查看源代码。 对于其他人,请参阅教程 Playground,了解该库的主要概念和用例的初学者级介绍。
SwiftCheck 是一个测试库,可以自动生成随机数据来测试程序属性。 属性是算法或数据结构的特定方面,在给定的输入数据集下必须是不变的,基本上是“类固醇”上的 XCTAssert
。 在此之前,我们只能定义以 test
为前缀并断言的方法,而 SwiftCheck 允许将程序属性和测试视为数据。
要定义程序属性,forAll
量词与类型签名一起使用,例如 (A, B, C, ... Z) -> Testable where A : Arbitrary, B : Arbitrary ... Z : Arbitrary
。 SwiftCheck 为大多数 Swift 标准库类型实现了 Arbitrary
协议,并为 Bool
和其他几个相关类型实现了 Testable
协议。 例如,如果我们想测试每个整数都等于自身的属性,我们将这样表达
func testAll() {
// 'property' notation allows us to name our tests. This becomes important
// when they fail and SwiftCheck reports it in the console.
property("Integer Equality is Reflexive") <- forAll { (i : Int) in
return i == i
}
}
对于一个不那么牵强的例子,这是一个程序属性,用于测试数组标识在双重反转下是否成立
property("The reverse of the reverse of an array is that array") <- forAll { (xs : [Int]) in
// This property is using a number of SwiftCheck's more interesting
// features. `^&&^` is the conjunction operator for properties that turns
// both properties into a larger property that only holds when both sub-properties
// hold. `<?>` is the labelling operator allowing us to name each sub-part
// in output generated by SwiftCheck. For example, this property reports:
//
// *** Passed 100 tests
// (100% , Right identity, Left identity)
return
(xs.reversed().reversed() == xs) <?> "Left identity"
^&&^
(xs == xs.reversed().reversed()) <?> "Right identity"
}
因为 SwiftCheck 不需要测试返回 Bool
,只需要 Testable
,我们可以轻松地为复杂属性生成测试
property("Shrunken lists of integers always contain [] or [0]") <- forAll { (l : [Int]) in
// Here we use the Implication Operator `==>` to define a precondition for
// this test. If the precondition fails the test is discarded. If it holds
// the test proceeds.
return (!l.isEmpty && l != [0]) ==> {
let ls = self.shrinkArbitrary(l)
return (ls.filter({ $0 == [] || $0 == [0] }).count >= 1)
}
}
属性甚至可以依赖于其他属性
property("Gen.one(of:) multiple generators picks only given generators") <- forAll { (n1 : Int, n2 : Int) in
let g1 = Gen.pure(n1)
let g2 = Gen.pure(n2)
// Here we give `forAll` an explicit generator. Before SwiftCheck was using
// the types of variables involved in the property to create an implicit
// Generator behind the scenes.
return forAll(Gen.one(of: [g1, g2])) { $0 == n1 || $0 == n2 }
}
你所要做的就是弄清楚要测试什么。 SwiftCheck 将处理剩下的事情。
QuickCheck 的独特之处在于缩减测试用例的概念。 当使用任意数据进行模糊测试时,SwiftCheck 不会简单地停止失败的测试,而是会开始将导致测试失败的数据缩小到最小的反例。
例如,以下函数使用埃拉托色尼筛法生成小于某个 n 的素数列表
/// The Sieve of Eratosthenes:
///
/// To find all the prime numbers less than or equal to a given integer n:
/// - let l = [2...n]
/// - let p = 2
/// - for i in [(2 * p) through n by p] {
/// mark l[i]
/// }
/// - Remaining indices of unmarked numbers are primes
func sieve(_ n : Int) -> [Int] {
if n <= 1 {
return []
}
var marked : [Bool] = (0...n).map { _ in false }
marked[0] = true
marked[1] = true
for p in 2..<n {
for i in stride(from: 2 * p, to: n, by: p) {
marked[i] = true
}
}
var primes : [Int] = []
for (t, i) in zip(marked, 0...n) {
if !t {
primes.append(i)
}
}
return primes
}
/// Short and sweet check if a number is prime by enumerating from 2...⌈√(x)⌉ and checking
/// for a nonzero modulus.
func isPrime(n : Int) -> Bool {
if n == 0 || n == 1 {
return false
} else if n == 2 {
return true
}
let max = Int(ceil(sqrt(Double(n))))
for i in 2...max {
if n % i == 0 {
return false
}
}
return true
}
我们想测试我们的筛子是否正常工作,所以我们使用以下属性通过 SwiftCheck 运行它
import SwiftCheck
property("All Prime") <- forAll { (n : Int) in
return sieve(n).filter(isPrime) == sieve(n)
}
这会在我们的测试日志中产生以下内容
Test Case '-[SwiftCheckTests.PrimeSpec testAll]' started.
*** Failed! Falsifiable (after 10 tests):
4
表明我们的筛子在输入数字 4 时失败了。 快速回顾一下描述筛子的注释,立即发现了错误
- for i in stride(from: 2 * p, to: n, by: p) {
+ for i in stride(from: 2 * p, through: n, by: p) {
再次运行 SwiftCheck 会报告所有 100 个随机案例的成功筛选
*** Passed 100 tests
SwiftCheck 为 Swift 标准库中的大多数类型实现了随机生成。 任何希望参与测试的自定义类型都必须符合包含的 Arbitrary
协议。 对于大多数类型,这意味着提供一种生成随机数据并缩减为空数组的自定义方法。
例如
import SwiftCheck
public struct ArbitraryFoo {
let x : Int
let y : Int
public var description : String {
return "Arbitrary Foo!"
}
}
extension ArbitraryFoo : Arbitrary {
public static var arbitrary : Gen<ArbitraryFoo> {
return Gen<(Int, Int)>.zip(Int.arbitrary, Int.arbitrary).map(ArbitraryFoo.init)
}
}
class SimpleSpec : XCTestCase {
func testAll() {
property("ArbitraryFoo Properties are Reflexive") <- forAll { (i : ArbitraryFoo) in
return i.x == i.x && i.y == i.y
}
}
}
还有一个 Gen.compose
方法,允许你从多个生成器中程序化地组合值,以构造类型的实例
public static var arbitrary : Gen<MyClass> {
return Gen<MyClass>.compose { c in
return MyClass(
// Use the nullary method to get an `arbitrary` value.
a: c.generate(),
// or pass a custom generator
b: c.generate(Bool.suchThat { $0 == false }),
// .. and so on, for as many values and types as you need.
c: c.generate(), ...
)
}
}
Gen.compose
也可以用于只能使用 setter 自定义的类型
public struct ArbitraryMutableFoo : Arbitrary {
var a: Int8
var b: Int16
public init() {
a = 0
b = 0
}
public static var arbitrary: Gen<ArbitraryMutableFoo> {
return Gen.compose { c in
var foo = ArbitraryMutableFoo()
foo.a = c.generate()
foo.b = c.generate()
return foo
}
}
}
对于其他一切,SwiftCheck 定义了许多组合器,以尽可能简化自定义生成器的使用
let onlyEven = Int.arbitrary.suchThat { $0 % 2 == 0 }
let vowels = Gen.fromElements(of: [ "A", "E", "I", "O", "U" ])
let randomHexValue = Gen<UInt>.choose((0, 15))
let uppers = Gen<Character>.fromElements(in: "A"..."Z")
let lowers = Gen<Character>.fromElements(in: "a"..."z")
let numbers = Gen<Character>.fromElements(in: "0"..."9")
/// This generator will generate `.none` 1/4 of the time and an arbitrary
/// `.some` 3/4 of the time
let weightedOptionals = Gen<Int?>.frequency([
(1, Gen<Int?>.pure(nil)),
(3, Int.arbitrary.map(Optional.some))
])
有关许多复杂或“真实世界”生成器的实例,请参见ComplexSpec.swift
。
SwiftCheck 支持 OS X 10.9+ 和 iOS 7.0+。
SwiftCheck 可以通过两种方式包含
使用 Swift Package Manager
Package.swift
文件的 dependencies 部分.package(url: "https://github.com/typelift/SwiftCheck.git", from: "0.8.1")
使用 Carthage
carthage update
Frameworks
使用 CocoaPods
$ pod install
。框架
SwiftCheck 在 MIT 许可证下发布。