Hedgehog includes a state machine testing module (Hedgehog.Stm) for testing stateful systems against an abstract model. This approach can find subtle bugs, especially concurrency issues, that are hard to catch with simple property tests.
State machine testing works by:
You implement the Hedgehog.Stm.Spec module type. Here's a complete example testing a mutable counter:
open Hedgehog
(* The system under test: a simple mutable counter *)
module Counter = struct
type t = { mutable value : int }
let create () = { value = 0 }
let get c = c.value
let incr c = c.value <- c.value + 1
let decr c = c.value <- c.value - 1
let reset c = c.value <- 0
end
module Counter_spec = struct
type cmd = Get | Incr | Decr | Reset
type state = int
type sut = Counter.t
type result = Int of int | Unit
let show_cmd = function
| Get -> "get"
| Incr -> "incr"
| Decr -> "decr"
| Reset -> "reset"
let show_result = function
| Int n -> string_of_int n
| Unit -> "()"
let gen_cmd _state =
Gen.element [Get; Incr; Decr; Reset]
let shrink_cmd _cmd = Seq.empty
let init_state = 0
let init_sut () = Counter.create ()
let cleanup _sut = ()
let next_state cmd state =
match cmd with
| Get -> state
| Incr -> state + 1
| Decr -> state - 1
| Reset -> 0
let precond _state _cmd = true
let run cmd sut =
match cmd with
| Get -> Int (Counter.get sut)
| Incr -> Counter.incr sut; Unit
| Decr -> Counter.decr sut; Unit
| Reset -> Counter.reset sut; Unit
let postcond cmd state result =
match cmd, result with
| Get, Int n -> n = state
| Get, _ -> false
| _, Unit -> true
| _, _ -> false
end
module Counter_test = Stm.Make(Counter_spec)open Hedgehog
module Counter = struct
type t = { mutable value : int }
let create () = { value = 0 }
let get c = c.value
let incr c = c.value <- c.value + 1
let decr c = c.value <- c.value - 1
let reset c = c.value <- 0
end
module Counter_spec = struct
type cmd = Get | Incr | Decr | Reset
type state = int
type sut = Counter.t
type result = Int of int | Unit
let show_cmd = function
| Get -> "get" | Incr -> "incr" | Decr -> "decr" | Reset -> "reset"
let show_result = function
| Int n -> string_of_int n | Unit -> "()"
let gen_cmd _state = Gen.element [Get; Incr; Decr; Reset]
let shrink_cmd _cmd = Seq.empty
let init_state = 0
let init_sut () = Counter.create ()
let cleanup _sut = ()
let next_state cmd state =
match cmd with
| Get -> state | Incr -> state + 1 | Decr -> state - 1 | Reset -> 0
let precond _state _cmd = true
let run cmd sut =
match cmd with
| Get -> Int (Counter.get sut) | Incr -> Counter.incr sut; Unit
| Decr -> Counter.decr sut; Unit | Reset -> Counter.reset sut; Unit
let postcond cmd state result =
match cmd, result with
| Get, Int n -> n = state | Get, _ -> false | _, Unit -> true | _, _ -> false
end
module Counter_test = Stm.Make(Counter_spec)
let () =
let prop = Counter_test.sequential () in
Property.check prop |> ignoreThe sequential test generates random command sequences, executes them one at a time, and checks postconditions after each step. If a postcondition fails, the sequence is shrunk to find the minimal failing prefix.
Parallel testing detects concurrency bugs by running commands concurrently and checking that the results are linearizable — that there exists some sequential ordering of the commands that explains the observed results:
open Hedgehog
module Counter = struct
type t = { mutable value : int }
let create () = { value = 0 }
let get c = c.value
let incr c = c.value <- c.value + 1
let decr c = c.value <- c.value - 1
let reset c = c.value <- 0
end
module Counter_spec = struct
type cmd = Get | Incr | Decr | Reset
type state = int
type sut = Counter.t
type result = Int of int | Unit
let show_cmd = function
| Get -> "get" | Incr -> "incr" | Decr -> "decr" | Reset -> "reset"
let show_result = function
| Int n -> string_of_int n | Unit -> "()"
let gen_cmd _state = Gen.element [Get; Incr; Decr; Reset]
let shrink_cmd _cmd = Seq.empty
let init_state = 0
let init_sut () = Counter.create ()
let cleanup _sut = ()
let next_state cmd state =
match cmd with
| Get -> state | Incr -> state + 1 | Decr -> state - 1 | Reset -> 0
let precond _state _cmd = true
let run cmd sut =
match cmd with
| Get -> Int (Counter.get sut) | Incr -> Counter.incr sut; Unit
| Decr -> Counter.decr sut; Unit | Reset -> Counter.reset sut; Unit
let postcond cmd state result =
match cmd, result with
| Get, Int n -> n = state | Get, _ -> false | _, Unit -> true | _, _ -> false
end
module Counter_test = Stm.Make(Counter_spec)
let () =
let prop = Counter_test.parallel () in
Property.check prop |> ignoreFor the simple counter above, parallel testing would likely find a bug since the incr and decr operations are not atomic. In a real application, you'd use locks or atomic operations and verify the implementation is correct.
The Hedgehog.Stm.Spec module type requires:
Field | Type | Description |
|---|---|---|
| type | Command variant type |
| type | Abstract model state |
| type | System under test type |
| type | Command result type |
|
| Pretty-print commands |
|
| Pretty-print results |
|
| Generate commands given current state |
|
| Shrink commands |
|
| Initial model state |
|
| Create fresh system under test |
|
| Tear down the system |
|
| Model state transition |
|
| Is command valid in state? |
|
| Execute command on real system |
|
| Check result against model |
gen_cmd's state parameter to only generate valid commands. For example, don't generate "pop" on an empty stack.precond as a safety net. Even with state-aware generation, shrinking might produce invalid sequences. precond filters these out.