hedgehog-ocaml

Release with confidence.

GitHub CI

Hedgehog automatically generates a comprehensive array of test cases, exercising your software in ways human testers would never imagine.

Generate hundreds of test cases automatically, exposing even the most insidious of corner cases. Failures are automatically simplified, giving developers coherent, intelligible error messages.

Features

Example

The main module, Hedgehog, includes everything you need to get started writing property tests.

open Hedgehog

Once you have your imports set up, you can write a simple property:

let prop_reverse =
  Property.property Gen.(
    let* xs = list (Range.linear 0 100) alpha in
    return (fun () ->
      Property.assert_ (List.rev (List.rev xs) = xs)))

You can then run it:

let () =
  if Property.check prop_reverse then
    print_endline "All tests passed."
  else
    exit 1
+++ OK, passed 100 tests.
All tests passed.

When a property fails, Hedgehog automatically finds a minimal counterexample:

let prop_bad =
  Property.property Gen.(
    let* n = int (Range.linear 0 1000) in
    return (fun () ->
      Property.annotate (Printf.sprintf "n = %d" n);
      Property.assert_ (n < 500)))
*** Failed! Falsifiable (after 17 tests):
  n = 500
  Assertion failed

State Machine Testing

The Stm module lets you test stateful systems by defining a model specification and checking that the real implementation matches:

open Hedgehog

module Counter_spec = struct
  type cmd = Incr | Decr | Get
  type state = int
  type sut = int ref
  type result = Unit | Int of int

  let show_cmd = function Incr -> "Incr" | Decr -> "Decr" | Get -> "Get"
  let show_result = function Unit -> "()" | Int n -> string_of_int n
  let gen_cmd _state = Gen.element [Incr; Decr; Get]
  let shrink_cmd _ = Seq.empty

  let init_state = 0
  let init_sut () = ref 0
  let cleanup _ = ()

  let next_state cmd state = match cmd with
    | Incr -> state + 1 | Decr -> state - 1 | Get -> state

  let precond _state _cmd = true

  let run cmd sut = match cmd with
    | Incr -> incr sut; Unit
    | Decr -> decr sut; Unit
    | Get -> Int !sut

  let postcond cmd state result = match cmd, result with
    | Get, Int n -> n = state
    | (Incr | Decr), Unit -> true
    | _ -> false
end

module Counter_stm = Stm.Make(Counter_spec)

Run a sequential test to check postconditions at each step:

let () =
  if Property.check (Counter_stm.sequential ()) then
    print_endline "Sequential: OK"

Run a parallel test to detect concurrency bugs via linearizability checking:

let () =
  if Property.check (Counter_stm.parallel ()) then
    print_endline "Parallel: OK"

Alcotest Integration

The hedgehog-alcotest package lets you run Hedgehog properties as Alcotest test cases:

opam install hedgehog-alcotest

Use Hedgehog_alcotest.to_alcotest to wrap a property:

let () =
  Alcotest.run "my-tests" [
    "properties", [
      Hedgehog_alcotest.to_alcotest "reverse involution"
        Hedgehog.(Property.property Gen.(
          let* xs = list (Range.linear 0 100) alpha in
          return (fun () ->
            Property.assert_ (List.rev (List.rev xs) = xs))));

      Hedgehog_alcotest.to_alcotest "small lists"
        Hedgehog.(Property.property Gen.(
          let* xs = list (Range.linear 0 100) (int (Range.linear 0 1000)) in
          return (fun () ->
            Property.annotate (Printf.sprintf "xs has %d elements" (List.length xs));
            Property.assert_ (List.length xs < 5))));
    ]
  ]

Passing properties return normally. Failures call Alcotest.fail with the shrunk counterexample:

[FAIL]  properties  1  small lists.
*** Failed! Falsifiable (after 9 tests):
  xs has 5 elements
  Assertion failed

Building

opam install . --deps-only --with-test
dune build

Running Tests

dune runtest

How It Works

Hedgehog is built on integrated shrinking, the same foundation QCheck2 adopted in 2021:

What Hedgehog lacks:

  1. ppx derivation (QCheck has ppx_deriving_qcheck, JS Quickcheck [%quickcheck.generator: int list]) and
  2. function generation (QCheck's Observable/Fn, JS Quickcheck's Observer.t).

Both require writing generators explicitly.

See the Alternatives page for a detailed comparison.

Architecture

Module

Description

Hedgehog.Seed

Splittable PRNG built on OCaml 5's Random.State

Hedgehog.Tree

Rose tree with lazy children for integrated shrinking

Hedgehog.Shrink

Pure shrinking strategies (binary search, halving, list removal)

Hedgehog.Range

Size-dependent ranges (constant, linear, exponential)

Hedgehog.Gen

Generator monad with numeric, string, list, and choice combinators

Hedgehog.Property

Property runner with OCaml 5 effect-based assertions

Hedgehog.Stm

State machine testing with sequential and parallel checking

Documentation

Full documentation is available at https://tmcgilchrist.github.io/ocaml-hedgehog/.

To build and preview the docs site locally:

opam install odoc          # one-time: needs odoc >= 3.2.1
cd website && npm install  # one-time: install Astro/Starlight
make website-dev           # generate markdown, then start the dev server

The site will be available at http://localhost:4321/ocaml-hedgehog/.

The documentation source lives in doc/*.mld files. These are processed by odoc's markdown backend into Starlight-compatible markdown, then built into a static site with Astro. Generating the markdown needs odoc >= 3.2.1 and dune >= 3.22 (which added the @doc-markdown alias); building and testing the library itself has no such requirement.

Resources