QuickCheck: Property-Based Testing
This book has claimed the Functor and Monad laws matter since Chapter 9, without ever actually checking them. QuickCheck is how — and it's also the tool that started an entire testing movement other languages later copied wholesale.
Testing what should always be true, not just one example
An ordinary unit test checks one specific input against one specific expected output. QuickCheck asks for something more general: a property — a statement that should hold for every input of a given type — and then generates hundreds of random inputs trying to find one that breaks it.
import Test.QuickCheck
prop_reverseTwice :: [Int] -> Bool
prop_reverseTwice xs = reverse (reverse xs) == xs
ghci> quickCheck prop_reverseTwice
+++ OK, passed 100 tests.
prop_reverseTwice says nothing about any particular list — it says “for every list, reversing it twice gives the original back,” and quickCheck throws a hundred different randomly-generated lists at it, checking the claim holds for all of them. No test author sat down and picked those hundred lists by hand; QuickCheck generated them, varying length, contents, and edge cases (including the empty list) automatically.
The payoff this book has been building toward
Chapters 9 through 11 stated the Functor laws and the Monad laws as things instances are expected to satisfy — promises, never actually verified in this book until now. QuickCheck is exactly the tool for checking them:
prop_functorIdentity :: [Int] -> Bool
prop_functorIdentity xs = fmap id xs == xs
prop_functorComposition :: [Int] -> Bool
prop_functorComposition xs =
fmap ((*2) . (+1)) xs == (fmap (*2) . fmap (+1)) xs
ghci> quickCheck prop_functorIdentity
+++ OK, passed 100 tests.
ghci> quickCheck prop_functorComposition
+++ OK, passed 100 tests.
And the very law this book’s previous chapter just introduced — Monoid associativity — is just as directly testable:
prop_semigroupAssoc :: [Int] -> [Int] -> [Int] -> Bool
prop_semigroupAssoc a b c = (a <> b) <> c == a <> (b <> c)
ghci> quickCheck prop_semigroupAssoc
+++ OK, passed 100 tests.
These aren’t proofs — a hundred random tests can’t rule out every possible counterexample the way a formal proof would — but they’re a genuinely strong, automatic, repeatable check that runs in milliseconds every time the code changes, catching the overwhelming majority of law violations a human reviewer would otherwise have to spot by inspection.
When a property fails: shrinking
The genuinely clever part shows up the moment a property is actually false. A random counterexample QuickCheck happens to generate first might be a long, ugly, hard-to-read list — not exactly illuminating. QuickCheck doesn’t just report that raw case; it shrinks it, searching for the smallest, simplest input that still fails.
Figure: Every candidate along the way still fails the property — QuickCheck keeps trying smaller variations until nothing simpler still fails, then reports that, instead of the original, harder-to-read random case.
prop_broken :: [Int] -> Bool
prop_broken xs = length xs < 5 -- obviously false for longer lists
ghci> quickCheck prop_broken
*** Failed! Falsified (after some tests and shrinks):
[0,0,0,0,0]
Instead of reporting whatever random 11-element list of mixed positive and negative numbers happened to trigger the failure first, QuickCheck narrows it down — dropping elements, shrinking the remaining values toward zero — until it lands on [0,0,0,0,0]: the smallest, simplest list that’s still long enough to break the property. That five-element list of zeros is immediately readable; the original random counterexample likely wasn’t.
Generating values: the Arbitrary class
QuickCheck’s random generation is itself just an ordinary typeclass:
class Arbitrary a where
arbitrary :: Gen a
Built-in instances cover Int, Bool, lists, tuples, and more — but custom types need their own instance, usually built directly on top of ones that already exist. Common Algorithms’ binary search tree is a good example:
instance Arbitrary a => Arbitrary (BST a) where
arbitrary = do
xs <- arbitrary
return (foldr insert Leaf xs)
prop_insertMember :: Int -> BST Int -> Bool
prop_insertMember x t = member x (insert x t)
ghci> quickCheck prop_insertMember
+++ OK, passed 100 tests.
arbitrary for BST a generates a random list (reusing the list’s own Arbitrary instance, itself built on Int’s), then builds a tree out of it with the insert this book already wrote — random trees, entirely for free, from a structure barely five lines long.
quickCheck by default only runs 100 tests per property — often plenty, but genuinely not a guarantee, especially for properties whose counterexamples are rare or require large, unusual inputs to surface. quickCheckWith stdArgs { maxSuccess = 1000 } (or higher) raises the count for properties that deserve extra scrutiny; no finite number of random tests can ever replace an actual proof, but a much larger count meaningfully narrows the gap for properties where getting it wrong would be expensive.
QuickCheck, first released in 1999, didn’t just influence testing in Haskell — it started an entire cross-language movement. Python’s Hypothesis, Rust’s proptest, JavaScript’s fast-check, and Scala’s ScalaCheck all directly credit QuickCheck as their model, and “property-based testing” as a recognized discipline distinct from example-based unit testing traces back specifically to this library. Few individual Haskell tools have shaped how an entire industry thinks about testing as directly as this one.
The real shift QuickCheck asks for isn’t a new syntax to learn — it’s a change in what “testing” means: instead of listing examples you already know the answer to, you write down what should be true in general, and let the machine go looking for the case you didn’t think of.