May 2021
“QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs” Koen Claessen and John Hughes (ICFP’2000).
Web site:
http://www.cse.chalmers.se/~rjmh/QuickCheck/
Introductory tutorial:
https://www.schoolofhaskell.com/user/pbv/an-introduction-to-quickcheck-testing
Ask GHCi to check this with randomly generated lists:
If the property fails, then QuickCheck prints out a counter-example.
import Test.QuickCheck
prop_wrong :: [Int] -> Bool
prop_wrong xs = reverse xs == xs
Main> quickCheck prop_rev_id
*** Failed! Falsifiable
(after 6 tests and 6 shrinks):
[0,1]prop_Means \[ \text{reverse}~(\text{reverse}~ xs) = xs, \quad \forall xs :\hspace{-0.1ex}: [\text{Int}] \]
insert :: Ord a => a -> [a] -> [a]
insert x [] = [x]
insert x (y:ys)
| x<y = x:y:ys
| otherwise = y:insert x ys
We want to specify that insertion preserves ordering, i.e.
\[ ordered~xs \implies ordered~(insert~x~xs), \quad \forall x, xs \]
The operator ==> allows expressing such conditional properties.
prop_ins_ord :: Int -> [Int] -> Property
prop_ins_ord x xs
= ordered xs ==> ordered (insert x xs)
-- NB: the result type is now Property
-- auxiliary definition
-- to test if a list is in ascending order
ordered :: Ord a => [a] -> Bool
ordered (x:y:xs) = x<=y && ordered (y:xs)
ordered _ = True> quickCheck prop_ins_ord
*Main Data.List> quickCheck prop_ins_ord
*** Gave up! Passed only 72 tests.What happened?
==> discards test cases that do not satisfy the preconditionWe can use verboseCheck to show the actual test cases:
This too verbose; it is better to collect statistical data on test cases.
prop_ins_ord :: Int -> [Int] -> Property
prop_ins_ord x xs
= collect (length xs) $
ordered xs ==> ordered (insert x xs)38% were the empty list
25% were single element lists
29% were two element lists
Only 6% were 3 or 4 elements!
A better approach: explicitly restrict test cases using a generator:
NB: this forAll is just a function defined in the QuickCheck library.
For our example we can use a generator called orderedList for lists of values in ascending order (also defined in the library):
prop_ins_ord2 :: Int -> Property
prop_ins_ord2 x
= forAll orderedList $
\xs -> ordered (insert x xs)Using custom generator ensures better distributed test cases.
prop_ins_ord2 x =
forAll orderedList $ \xs ->
collect (length xs) $
ordered (insert x xs)
Main> quickCheck prop_ins_ord2
6% 0
5% 1
4% 32
4% 22
4% 2
...Gen T is the type for generators of values of type TGen is a monad (and applicative functor)do-notation or applicative operators for defining generatorsGen monad hides:
choose :: Random a => (a,a) -> Gen a
-- random choice from interval
elements :: [a] -> Gen a
-- random choice from listExamples:
-- using do-notation
sum_dice :: Gen Int
sum_dice = do d1 <- dice
d2 <- dice
return (d1+d2)
-- using applicative operators
sum_dice :: Gen Int
sum_dice = (+) <$> dice <*> diceWe can use sample to show random samples of a generator.
oneof :: [Gen a] -> Gen a
-- uniform random choice
frequency :: [(Int,Gen a)] -> Gen a
-- random choice with frequencyExamples:
fruit = elements ["apple", "orange", "banana"]
sweet = elements ["cake", "ice-cream"]
-- fruit and sweets equally probable
desert = oneof [fruit, sweet]
-- healthy option: 4/5 fruits, 1/5 sweets
healthy = frequency [(4,fruit),(1,sweet)]Main> sample healthy
"orange"
"ice-cream"
"apple"
"banana"
"ice-cream"
"apple"
"apple"
"banana"
"orange"
"banana"
"orange"We can write a custom generator using basic combinators and do-notation.
data Person = Person String Int
genPerson :: Gen Person
genPerson =
do name <- elements ["Alice", "Bob",
"David", "Carol"]
age <- choose (10, 99)
return (Person name age)Main> sample genPerson
Person "Alice" 77
Person "David" 87
Person "Alice" 67
Person "Bob" 51
Person "David" 91
Person "David" 36
Person "Alice" 47
Person "Alice" 84
Person "Carol" 36
Person "Bob" 94listOf :: Gen a -> Gen [a]
-- lists of random length
listOf1 :: Gen a -> Gen [a]
-- non-empty lists of random length
vectorOf :: Int -> Gen a -> Gen [a]
-- list of the given lengthMain> sample (vectorOf 5 $ elements "aeiou")
"ueuuo"
"aioua"
"iouui"
"iaoue"
"aeaii"
"iiuio"
"uuaoe"
"ieeue"
"ouiei"
"uoeoo"
"ueaeo"We can specify a default generator for a type in the Arbitrary class.
QuickCheck defines Arbitrary instances for many basic types:
instance Arbitrary Int
instance Arbitrary Bool
instance Arbitrary a => Arbitrary [a]
instance (Arbitrary a,
Arbitrary b) => Arbitrary (a,b)
...We can define instances of Arbitrary for new types:
instance Arbitrary Person where
arbitrary = genPerson
genPerson :: Gen Person
genPerson = ... -- as beforeInt and Integer - ignored for enumerations (e.g. Bool or Char);sized combinator to access the size parameter when writing generators of recursive data structures:instance Arbitrary a => Arbitrary (Tree a) where
arbitrary = sized genTree
-- | generate a tree of given size
-- size ~ number of branch nodes
genTree :: Arbitrary a => Int -> Gen (Tree a)
genTree size
| size>0 = do v <- arbitrary
l <- genTree (size`div`2)
r <- genTree (size`div`2)
return (Branch v l r)
| otherwise = return Leaf -- size ~ upper bound of inner nodes
genTree' :: Arbitrary a => Int -> Gen (Tree a)
genTree' size
| size>0 = frequency [(4, genBranch),
(1, return Leaf)]
| otherwise = return Leaf
where
genBranch = do v <- arbitrary
l <- genTree' (size`div`2)
r <- genTree' (size`div`2)
return (Branch v l r)prop_wrong :: [Int] -> [Int] -> Bool
prop_wrong xs ys =
reverse (xs++ys) == reverse xs ++ reverse ysQuickCheck randomly generates a counter-example, e.g. xs = [2,2] and ys = [1].
Shrinking tries to find a smaller counter-example recursively:
xs and ys;This process is iterated until no smaller counter-example is found.

We get xs=[0], ys=[1] after two shrinking steps; more shrinking does not yield counter-examples (local minimum).
shrink functionThe shrinking function is defined in the Arbitrary type class.
class Arbitrary a where
arbitrary :: Gen a -- generator
shrink :: a -> [a] -- shrink function
shrink _ = [] -- default: no shrinkingshrink should give a (finite) list of “smaller” values to test for counter-examples.
The default gives an empty list — i.e. shrinking is disabled.
Heuristic to shrink a branching node:
No further shrinking when we reach a leaf.
instance Arbitrary a => Arbitrary (Tree a) where
arbitrary = ... -- as before
shrink = shrinkTree
shrinkTree :: Arbitrary a => Tree a -> [Tree a]
shrinkTree (Branch v l r) =
[Leaf, l, r] ++
[Branch v l' r | l'<-shrinkTree l] ++
[Branch v l r' | r'<-shrinkTree r] ++
[Branch v' l r | v'<-shrink v]
shrinkTree _ = [] -- no shrinking for leavesshrink function should reduce a value by a single stepshrink function until it cannot find a smaller counterexample
rotateL, rotateR :: Tree a -> Tree a
rotateL (Branch x t1 (Branch y t2 t3))
= Branch y (Branch x t1 t2) t3
rotateL t = t -- nothing to do
rotateR (Branch x (Branch y t1 t2) t3)
= Branch x t1 (Branch y t2 t3)
rotateR t = t -- nothing to doprop_rotateL :: Tree Int -> Bool
prop_rotateL t
= toList t == toList (rotateL t)
prop_rotateR :: Tree Int -> Bool
prop_rotateR t
= toList t == toList (rotateR t)
-- list tree values in order
toList :: Tree a -> [a]
toList Leaf = []
toList (Branch v l r)
= toList l ++ [v] ++ toList r*Main> quickCheck prop_rotateL
+++ OK, passed 100 tests.
Main> quickCheck prop_rotateR
*** Failed! Falsifiable
(after 3 tests and 3 shrinks):
Branch 1 (Branch 0 Leaf Leaf) Leaf
We found an error!
rotateR is incorrect (x and y are switched around)Using newtype allows customizing the generators for specific uses.
vs.
data Expr = Var Name | ...
newtype Name = Name String
instance Arbitrary Name where
arbitrary = do n<-elements ["a","b","c","d"]
return (Name n)newtype NonNegative a = NonNegative a
newtype NonZero a = NonZero a
newtype Positive a = Positive a
newtype NonEmptyList a = NonEmpty [a]
newtype OrderedList a = Ordered [a]Allow restricting the range of generated values: