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:

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:

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:

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