【函数式 Swift】QuickCheck

QuickCheck 是一个用于随机测试的 Haskell 工具库,本文将基于原书中的案例以及函数式编程方法讨论如何构建 Swift 版本的 QuickCheck 库。

注:在学习本章内容以前,笔者没有学习过 Haskell,也没有使用过 QuickCheck,本文是通过原书及一些网络资料学习后的心得,如有错误或遗漏,欢迎批评指正。


QuickCheck 概述

QuickCheck 项目始于 1999 年,作者 Koen Claessen 和 John Hughes 在其论文《QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs》中对测试工具应该具备的特性进行了讨论,主要有以下两点:

  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."

下面来具体了解一下 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.

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 个组成部分:

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值