Motivation
Motivation
Why does Hedgehog exist when OCaml already has QCheck?
Hedgehog takes a fundamentally different approach to property-based testing. This page explains the design decisions and their consequences.
The shrinking problem
Traditional property-based testing libraries (QuickCheck, QCheck, ScalaCheck) separate generation from shrinking. You write a generator to produce random values, and a separate shrink function to make failing values smaller:
(* Traditional approach — generation and shrinking are separate *)type 'a gen = Random.t -> 'atype 'a shrink = 'a -> 'a listThis works, but has serious drawbacks:
- You must write shrink functions manually. For complex types this is tedious and error-prone. Forget a shrink function and you get enormous counterexamples.
- Shrinking can produce invalid values. If your generator only produces sorted lists, naive shrinking might produce unsorted ones, causing spurious failures.
- Composed generators lose shrinking. When you
bindtwo generators, the shrink functions don’t automatically compose. The shrunk values from the first generator aren’t fed through the second.
Integrated shrinking
Hedgehog solves this by integrating shrinking into generation. A generator doesn’t just produce a value — it produces a rose tree of values, where the root is the generated value and the children are progressively simpler alternatives:
type 'a tree = Node of 'a * 'a tree Seq.ttype 'a gen = int -> Seed.t -> 'a tree optionWhen a property fails, Hedgehog walks down the tree, trying simpler values until it finds the smallest one that still fails. This means:
- Every generator shrinks automatically. No manual shrink functions.
- Shrinking respects generator invariants. A filtered generator’s shrink tree only contains values that pass the filter.
- Composition preserves shrinking. When you
bindtwo generators, the resulting shrink tree interleaves shrinks from both.
Range-controlled generation
Most property-based testing libraries couple value range to the size parameter in an opaque way. Hedgehog makes this explicit with Hedgehog.Range:
open Hedgehog
(* A range from 0 to 100, shrinking towards 0 *)let _ = Range.linear 0 100
(* A range from -50 to 50, shrinking towards 0 *)let _ = Range.linear_from 0 (-50) 50
(* Exponential growth — more small values, fewer large ones *)let _ = Range.exponential 0 1000Ranges encode three concepts:
- Origin — the value to shrink towards
- Bounds — the limits of generation, potentially size-dependent
- Scaling — how bounds grow with the size parameter (constant, linear, or exponential) This makes it easy to control value distribution and shrink direction independently.
Effects-based assertions
OCaml 5’s algebraic effects provide a clean way to express test assertions without threading state:
open Hedgehog
let () = Property.check Property.(property Gen.( let* x = int (Range.linear 1 100) in let* y = int (Range.linear 1 100) in return (fun () -> annotate (Printf.sprintf "x = %d, y = %d" x y); assert_ (x + y > 0)))) |> ignoreThe annotate, assert_, cover, and other operations are effects handled by the property runner. This keeps the generator (which builds the shrink tree) cleanly separated from the test body (which performs effects).
Further reading
- Gens N’ Roses — Jacob Stanley’s talk on the design of Haskell Hedgehog
tutorial— Hands-on guide to using Hedgehogalternatives— Detailed comparison with QCheck and Jane Street Quickcheck