github
QuickCheck is a Haskell library for random testing. Rather than stand-alone unit tests where each part relies on specific inputs to test whether a function is correct, QuickCheck allows you to describe the abstract features of a function and generate tests to verify those features. When a feature passes testing, there is no need to prove it correct. More specifically, QuickCheck aims to find critical conditions that prove feature errors.
Checking this with QuickCheck is like calling check: The check function does this by calling plusIsCommutative over and over again, passing two random integer values each time. If the statement is not true, it will print out the input value that caused the test to fail. The point here is that we can use functions that return Bool to describe abstract properties of our code (such as the commutative law)
Verify that addition is an operation satisfying the commutative law
func plusIsCommutative(x: Int, y: Int) -> Bool {
returnX + y == y + x}"Plus should be commutative", plusIsCommutative)
Copy the code
Of course, not all tests pass
Define a statement to say that subtraction satisfies the commutative law
func minusIsCommutative(x: Int, y: Int) -> Bool {
return x - y == y - x
}
// check("Minus should be commutative", minusIsCommutative)
// print: "Minus should be commutative" does not hold: (3, 2)
Copy the code
Using Swift’s trailing closure syntax, we can also write tests directly without having to define a property (such as plusIsCommutative or minusIsCommutative) separately:
check("Additive identity") { (x: Int) in x + 0 == x }
// print: "Additive identity" passed 10 tests.
Copy the code
1. Build QuickCheck
To build the Swift version of QuickCheck, we need to do a few things:
-
We need a way to generate different types of random numbers
-
Implement the check function and then pass a random number to its property arguments
-
If a test fails, we want the input value of the test to be as small as possible.
-
Do some extra work to ensure that the validation function works with types with generics
1.1 Generating Random Numbers
Define a protocol that expresses how random numbers are generated
protocol Arbitrary: Smaller {
static func arbitrary() -> Self
}
Copy the code
extension Int: Arbitrary {
static func arbitrary() -> Int {
return Int(arc4random())
}
}
Copy the code
Random number generation
print(Int.arbitrary())
Copy the code
Randomly generate a number between 0 and 40 as the length of a character creation. Generates x random characters
extension Int {
static func random(from: Int, to: Int) -> Int {
returnfrom + (Int(arc4random()) % (to - from)) } } extension Character: Static func Arbitrary () -> Character {return Character(UnicodeScalar(Int.random(from: 65, to: 90))!)
}
}
func tabulate<A>(times: Int, transform: (Int) -> A) -> [A] {
return(0.. <times).map(transform)
}
extension String: Arbitrary {
static func arbitrary() -> String {
let randomLength = Int.random(from: 0, to: 40)
let randomCharacters = tabulate(times: randomLength) { _ in
Character.arbitrary()
}
return String(randomCharacters)
}
}
Copy the code
Generating random strings
print(String.arbitrary())
Copy the code
1.2 Implementing the check function
The check1 function consists of a simple loop that generates random input values for the properties to be checked at each iteration and then checks. As soon as a counterexample is found, print it out and return it immediately. Otherwise, the check1 function will report the number of tests successfully passed.
func check1<A: Arbitrary>(message: String, _ property: (A) -> Bool) -> () {
let numberOfInterations = 100
for _ in0.. <numberOfInterations {let value = A.arbitrary()
guard property(value) else {
print("\"\(message)\" doesnot hold: \(value)")
return
}
print("\"\(message)\" passed \(numberOfInterations) tests")}}Copy the code
Here’s an example:
extension CGFloat: Arbitrary {
static func arbitrary() -> CGFloat {
return CGFloat(Int.random(from: -100, to:100))
}
}
extension CGSize {
var area: CGFloat {
return width * height
}
}
extension CGSize: Arbitrary {
static func arbitrary() -> CGSize {
return CGSize(width: CGFloat.arbitrary(), height: CGFloat.arbitrary())
}
}
Copy the code
This example is a good example of when QuickCheck can be useful: it finds critical cases for us. If the size has one and only one negative value, our area function returns a negative value.
check1(message: "Area should be at least 0") { (size: CGSize) -> Bool in
size.area >= 0
}
// print: "Area should be at least 0"Doesnot hold: (36.0, 33.0)Copy the code
##2. Narrow it down
check1(message: "Every string starts with Hello") { (s: String) -> Bool in s.hasPrefix("Hello"} / /print: "Every string starts with Hello" doesnot hold: NRTYBRBSRNLOLGQPESSVKDMGRPTRXUOKRVE
Copy the code
Ideally, we want the failed input to be as simple as possible. In general, the smaller the scope of the counterexample, the easier it is to pinpoint which piece of code caused the failure.
Protocol Smaller {func Smaller () -> Self? }Copy the code
Extension Int: Smaller {// For integers, we try to divide by 2 until equal to 0 func Smaller () -> Int? {return self == 0 ? nil : self / 2
}
}
print(100.smaller() as Any)
// print: Optional(50)
Copy the code
Extension String: Smaller {// For strings, remove the first character (unless the String is empty) func Smaller () -> String? {return isEmpty ? nil : String(self.dropFirst())
}
}
Copy the code
Redefining the Arbitrary protocol and extending the Smaller protocol
protocol Arbitrary: Smaller {
static func arbitrary() -> Self
}
Copy the code
Narrow it down repeatedly
Takes a condition and an initial value, and calls itself repeatedly as long as the condition is true
func iterateWhile<A>(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
Repeatedly narrow down the scope of the counterexamples found in the test
func check2<A: Arbitrary>(message: String, _ property: (A) -> Bool) -> () {// Generate random input values, check if they satisfy the property parameters, and reduce the range repeatedly if A counter example is foundlet numberOfIterations = 10
for _ in0.. <numberOfIterations {let value = A.arbitrary()
guard property(value) else {
letsmallerValue = iterateWhile(condition: { ! property($0) }, initial: value) {
$0.smaller()
}
print("\"\(message)\" doesnot hold: \(smallerValue)")
return}}print("\"\(message)\" passed \(numberOfIterations) tests.")}Copy the code
Random number group
The check2 function supports only Int and String
Generate quick sort of functional version of random number group
func qsort(array: [Int]) -> [Int] {
var tmpArr = array
if array.isEmpty { return[]}let pivot = tmpArr.removeFirst()
let lesser = tmpArr.filter { $0 < pivot }
let greater = tmpArr.filter { $0 >= pivot }
let pivots = [pivot]
return qsort(array: lesser) + pivots + qsort(array: greater)
}
Copy the code
Removes the first item of the array
extension Array: Smaller { func smaller() -> [Element]? { guard ! isEmptyelse {
return nil
}
return Array(dropFirst())
}
}
Copy the code
Any type that follows the Arbitrary protocol will generate a random-length array
extension Array where Element: Arbitrary {
static func arbitrary() -> [Element] {
let randomLength = Int(arc4random() % 50)
return tabulate(times: randomLength) { _ in
Element.arbitrary()
}
}
}
Copy the code
Because of some limitations, we couldn’t write an extension that would make Array follow the Arbitrary protocol. Start by defining an auxiliary structure that contains the two required functions
struct ArbitraryInstance<T> {
let arbitrary: () -> T
let smaller: (T) -> T?
}
Copy the code
CheckHelper is defined strictly according to the previous check2 function. The only difference between the two is where arbitrary and smaller are defined. In Check2, they are constrained by the generic
type, while in checkHelper, they are passed as shown in the ArbitraryInstance structure. This way, you have more flexibility.
func checkHelper<A>(arbitraryInstance: ArbitraryInstance<A>, _ prorerty: (A) -> Bool, _ message: String) -> () {
let numberOfIterations = 10
for _ in0.. <numberOfIterations {let value = arbitraryInstance.arbitrary()
guard prorerty(value) else {
let smallerValue = iterateWhile(condition: { (x: A) -> Bool in
return! prorerty(x) }, initial: value, next: arbitraryInstance.smaller)print("\"\(message)\" doesnot hold: \(smallerValue)")
return}}print("\"\(message)\" passed \(numberOfIterations) tests")
}
func check<X: Arbitrary>(message: String, property: (X) -> Bool) -> () {
let instance = ArbitraryInstance<X>(arbitrary: X.arbitrary() as! () -> X, smaller: { (x: X) -> X in
return x.smaller()!
})
checkHelper(arbitraryInstance: instance, property, message)
}
Copy the code
If you can’t define a Arbitrary instance you want, just like an array, you can override the check function and construct the ArbitraryInstance structure you want
func check<X: Arbitrary>(message: String, _ property: ([X]) -> Bool) -> () {
let instance = ArbitraryInstance(arbitrary: Array.arbitrary, smaller: { (x: [X]) in x.smaller() })
checkHelper(arbitraryInstance: instance, property, message)
}
Copy the code
To verify our implementation of quicksort, a large number of random numbers will be generated and passed to our tests
check(message: "qsort should behave like sort") { (x: [Int]) -> Bool in
return qsort(array: x) == x.sorted(by: { (x, y) -> Bool in
return x < y
})
}
// print: "qsort should behave like sort" passed 10 tests
Copy the code
3. Use the QuickCheck
If you use QuickCheck for test-driven development from the start, you’ll see that it has a huge impact on your code design. QuickCheck forces you to think about what abstract features your functions must satisfy and allows you to give a high-level specification. By initially thinking about an advanced QuickCheck specification, your code will evolve towards modularity and referential transparency. QuickCheck does not apply to stateful functions or APIs. Therefore, QuickCheck from the beginning to write test code will help keep the code clean.