Category Theory Key idea: Instead of writing individual example tests, describe what should be true for every input — then let the computer generate hundreds of cases trying to prove you wrong

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.

QuickCheck shrinking a failing random test case down to a minimal counterexample

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.

⚠Common Pitfall

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.

In the Wild

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.