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.
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 -> 'a type 'a shrink = 'a -> 'a list
This works, but has serious drawbacks:
bind two generators, the shrink functions don't automatically compose. The shrunk values from the first generator aren't fed through the second.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.t type 'a gen = int -> Seed.t -> 'a tree option
When a property fails, Hedgehog walks down the tree, trying simpler values until it finds the smallest one that still fails. This means:
bind two generators, the resulting shrink tree interleaves shrinks from both.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:
This makes it easy to control value distribution and shrink direction independently.
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).
tutorial — Hands-on guide to using Hedgehogalternatives — Detailed comparison with QCheck and Jane Street Quickcheck