SwiftCheck 0.12.0

SwiftCheck 0.12.0

测试已测试
Lang语言 SwiftSwift
许可证 MIT
发布最后发布2019年3月
SPM支持 SPM

Robert Widmann 维护。



SwiftCheck 0.12.0

  • CodaFi 和 pthariensflame

Carthage compatible Build Status Gitter chat

SwiftCheck

Swift 的 QuickCheck。

对于已经熟悉 Haskell 库的人来说,请查看源码。对于其他人,请参阅 教程 playground,以了解该库的主要概念和使用案例的入门级介绍。

简介

SwiftCheck 是一个测试库,它自动为测试程序属性生成随机数据。属性是算法或数据结构的一个特定方面,必须在给定的输入数据集下保持不变,基本上是增强版的 XCTAssert。以前我们只能定义以 test 为前缀的方法,然后进行断言,而 SwiftCheck 允许将程序属性和测试视为 数据

要定义程序属性,请使用类似于 (A, B, C, ... Z) -> Testable where A : Arbitrary, B : Arbitrary ... Z : Arbitrary 的类型签名与 forAll 求量词。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.reverse().reverse() == xs) <?> "Left identity"
        ^&&^
        (xs == xs.reverse().reverse()) <?> "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包管理器

  • 将SwiftCheck添加到您的Package.swift文件的依赖关系部分
.package(url: "https://github.com/typelift/SwiftCheck.git", from: "0.8.1")

使用Carthage

  • 将SwiftCheck添加到您的Cartfile中
  • 运行carthage update
  • 将相关的SwiftCheck副本拖入您的项目。
  • 展开“链接二进制库”阶段
  • 点击"+"并添加SwiftCheck
  • 点击左上角的"+"以添加一个“复制文件”构建阶段
  • 将目录设置为Frameworks
  • 点击"+"并添加SwiftCheck

使用CocoaPods

  • 我们的Pod添加到您的Podfile中。
  • 在您的项目目录中运行$ pod install

框架

  • 将SwiftCheck.xcodeproj拖入您的项目树中作为子项目
  • 在您的项目的“构建阶段”下,展开“目标依赖
  • 点击"+"并添加SwiftCheck
  • 展开“链接二进制库”阶段
  • 点击"+"并添加SwiftCheck
  • 点击左上角的"+"以添加一个“复制文件”构建阶段
  • 将目录设置为Frameworks
  • 点击"+"并添加SwiftCheck

许可证

SwiftCheck是在MIT许可证下发布的。