Skip to content

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 -> 'a
type 'a shrink = 'a -> 'a list

This 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 bind two 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.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:

  • 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 bind two 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 1000

Ranges 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))))
|> ignore

The 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 Hedgehog
  • alternatives — Detailed comparison with QCheck and Jane Street Quickcheck