QuickCheck is a Haskell library for random testing. This article discusses how to build QuickCheck for Swift based on the case studies in the original book and the functional programming methods.

Note: I have not studied this chapter beforeHaskellAnd I haven’t used itQuickCheck, this article is through the original book and some network materials after learning experience, if there are mistakes or omissions, welcome criticism and correction.


QuickCheck overview

The QuickCheck project was started in 1999 by Authors Koen Claessen and John Hughes in their paper QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs discusses the required features of A test Tool, including the following:

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

The two points are fairly straightforward: first, the tester should provide a standard that allows the test tool automation to determine the success of a use case, and then the test tool should be able to automate the generation of test cases based on that standard so that the application can be randomly tested.

“An important design goal was that QuickCheck should be lightweight.”

Here’s a look at QuickCheck, as described on Wikipedia:

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 was originally implemented as a library based on Haskell with the primary goal of assisting software testing by generating use cases. Its main functional logic is:

  1. First, programmers write assertions using QuickCheck to verify that a function satisfies its logical properties.
  2. QuickCheck then randomly generates test cases to make the above assertion fail;
  3. Then, once a failed use case is found, QuickCheck minimizes the input value of the failed use case to quickly locate the problem.
  4. Finally, output the test result: success, or the minimum set of use cases that will cause the test to fail.

Thus, a QuickCheck should contain the following four components:

  1. Random number generation;
  2. Use case success/failure validation criteria;
  3. Use case scope minimization;
  4. Test results output.

Build the Swift version QuickCheck

All we need to do to build the Swift version of QuickCheck is build these four components.

Random number generation

Random numbers here do not refer specifically to numeric types, but should support “all manner of” types such as characters, strings, etc. For this purpose, we can define a protocol for generating random numbers:

protocol Arbitrary {
    static func arbitrary() -> Self
}Copy the code

When you want to generate arbitrary numbers of any type, you simply follow the protocol and implement the corresponding arbitrary(). Take Int as an example:

extension Int: Arbitrary {
    static func arbitrary() -> Int {
        return Int(arc4random())
    }
}

print(Int.arbitrary()) / / "3212540033"Copy the code

Use case success/failure validation criteria

Use case success/failure verification criteria, namely the logical properties that a function should satisfy, is what the authors of QuickCheck call “determine whether a test is passed or failed”. Therefore, the property definition should look something like:

typealias property = A: Arbitrary -> Bool // Syntax errorCopy the code

Property validates the input random number and returns true on success and false on failure. QuickCheck repeatedly generates and validates random numbers to find a use case that failed validation. For this we also need a check function:

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!")}Copy the code

The check function has the following functions:

  1. Control cycles (for _ in 0 .. < numberOfIterations);
  2. Create random input (let value = A.arbitrary());
  3. Verify the success of the use case (guard property(value)).

Use case scope minimization

After completing the first two steps, we also need to narrow down the failed use cases so that we can more easily locate the problem code. So we want the random numbers entered to be reduced and the validation process rerunned.

To do this, we can define a Smaller protocol to reduce input (as we did in the original book). Similarly, we can extend the Protocol Arbitrary number to add reduction methods to it. Here we use the latter:

protocol Arbitrary {
    static func arbitrary() -> Self
    static func shrink(_ : Self) -> Self?
}Copy the code

The shrink function reduces the random input and returns it. However, we use an optional type for the return value. That is, some input cannot be reduced any longer, such as an empty array, where we need to return nil.

Now, we modify the above Int extension by adding the shrink function to it:

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)Copy the code

In the example above, for integers, we try to reduce them by dividing them by two until they equal zero.

In fact, use-case reduction is an iterative process, and may even be an “infinite” process, so we’ll replace this “infinite” reduction with a function:

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
}Copy the code

We can further modify the check function by reducing the input use case by calling iterateWhile when we find a failed use case:

func check_2<A: Arbitrary>(_ property: (A) -> Bool) -> Void {
    for _ in 0 ..< numberOfIterations {
        let value = A.arbitrary()
        guard property(value) else {
            // Shrink the use cases
            letsmallerValue = iterateWhile({ ! property($0)},initial: value) {
                A.shrink($0)
            }
            print("Failed Case: \(smallerValue).")
            return
        }
    }
    print("All cases passed!")}Copy the code

Test result output

In the output of test results, we did not do much more than simply output results, which will not be described here.

conclusion

QuickCheck allows us to quickly test functions and help locate problems in our code through use-case reduction. Test-driven development with QuickCheck also forces us to think about functions’ responsibilities and abstract features that need to be met, helping us design and develop modular, low-coupling programs.

After understanding the idea of QuickCheck, we built a simple Swift version of QuickCheck, which incorporates functional thinking, and we broke the whole problem down into four parts, Random number generation function, use case verification function, use case reduction function and check function are written respectively, so as to complete the QuickCheck function. But there is still a long way to go.

At present, some developers have completed a relatively complete Swift version of QuickCheck, named SwiftCheck, for practical application or further study.

The resources

  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

This article belongs to “functional Swift” reading notes series, synchronous update in Huizhao.win, welcome to pay attention to!