View Source Bond.PropertyTest (Bond v1.7.0)
Property-based testing helpers that drive Bond-contracted functions with random inputs.
Bond contracts (@pre, @post, check/1,2, @invariant) are runtime predicates that
already encode what "correct" looks like. Property-based testing usually has two hard
parts — generating inputs, and writing the oracle that distinguishes right from wrong
outputs. With Bond, the oracle is already there at every call site; PBT just feeds
random inputs in and lets the existing instrumentation raise on any violation.
Bond.PropertyTest adds three macros, one per testing shape:
contract_holds/2— single function. Pass a function reference and a list of generators (one per argument). The macro calls the function with random inputs; any contract violation fails the property and StreamData shrinks to a minimal counterexample.contract_holds &Math.sqrt/1, args: [StreamData.float(min: 0.0)]probe_contract/2— single function, boundary-driven. Likecontract_holds/2, but it mixes the boundary values implied by the function's@preinto your generators and filters out inputs that violate@pre(rather than failing on them), so the function's@postis the oracle and its precondition edges are probed deliberately.probe_contract &Account.withdraw/2, args: [account_gen(), StreamData.integer()]invariants_hold/2— stateful module sequence. Pass a struct module plus constructor / transformer / observer specs. The macro generates random sequences of operations over the struct and runs them; the module's@invariants (plus any per-function contracts) are the oracle across every reachable state.invariants_hold BoundedStack, constructors: [{:new, [StreamData.integer(1..100)]}], transformers: [{:push, [StreamData.term()]}, {:pop, []}], observers: [{:size, []}, {:peek, []}]
Setup
Bond.PropertyTest depends on
stream_data. It's listed as an optional
dependency in bond's mix file, so users opting into PBT add it to their own deps:
{:stream_data, "~> 1.0", only: [:dev, :test]}Then in a test file:
defmodule MyTest do
use ExUnit.Case
use Bond.PropertyTest
# contract_holds ... / invariants_hold ...
endIf stream_data is not available at compile time, use Bond.PropertyTest raises a
CompileError with instructions to add the dep.
Summary
Functions
When used in an ExUnit test module, brings in ExUnitProperties (for the underlying
property/2 and check all macros) and imports Bond.PropertyTest so contract_holds
and invariants_hold are available.
Generates an ExUnit property that calls a single function with random arguments and
verifies that Bond's contracts (preconditions, postconditions, checks, invariants)
are all satisfied.
Generates an ExUnit property that runs random sequences of operations over a struct
module and verifies that the module's @invariants (plus any per-function contracts)
hold across every reachable state.
Generates an ExUnit property that probes a function at its precondition boundaries: it mixes
the boundary values implied by the function's @pre (e.g. 0 and its neighbours for
@pre x >= 0) into your generators, discards any generated input that does not satisfy @pre,
and lets the function's own @post/check contracts be the oracle on the inputs that survive.
Functions
When used in an ExUnit test module, brings in ExUnitProperties (for the underlying
property/2 and check all macros) and imports Bond.PropertyTest so contract_holds
and invariants_hold are available.
Raises a CompileError at the use site if stream_data isn't available — see the
module docs.
Generates an ExUnit property that calls a single function with random arguments and
verifies that Bond's contracts (preconditions, postconditions, checks, invariants)
are all satisfied.
Pass a function reference and a list of generators, one per argument:
contract_holds &Math.sqrt/1, args: [StreamData.float(min: 0.0)]The macro expands to a property block. On each iteration it generates one value per
argument and calls the function. Any contract violation raised by Bond's runtime
instrumentation fails the property; StreamData then shrinks to the minimal
counterexample.
Useful for catching edge cases your example-based tests didn't cover. The function's contracts are the oracle — no separate assertion is needed.
For stateful testing over a struct module — random sequences of operations checked
against the module's @invariants — see invariants_hold/2.
Options
:args(required) — list ofStreamDatagenerators, one per function argument.:name(optional) — a string used as the property's description. Defaults to"contract_holds <source>".
Generates an ExUnit property that runs random sequences of operations over a struct
module and verifies that the module's @invariants (plus any per-function contracts)
hold across every reachable state.
This is Bond's stateful, sequence-based property testing. The invariants are a free oracle — they hold at every entry and exit, so there's no need to write an explicit per-operation model of expected behaviour, which is what makes stateful PBT cheap here.
Pass a struct module plus constructor, transformer, and observer specs. The macro generates random sequences of operations over the struct, threads state through them, and runs them.
invariants_hold BoundedStack,
constructors: [{:new, [StreamData.integer(1..100)]}],
transformers: [{:push, [StreamData.term()]}, {:pop, []}],
observers: [{:size, []}, {:peek, []}]Each spec is a list of {fun_name, [arg_generators]} tuples:
- Constructor — produces an initial struct. Called first in every sequence.
- Transformer — takes the current struct as its first argument plus generated
args, returns a new struct (
%Mod{}or{:ok, %Mod{}}). Advances the state. - Observer — takes the current struct plus generated args, returns anything. Doesn't advance state. The pre-invariant still fires on entry.
Return shape rules for constructors and transformers:
%Mod{}— becomes the new state.{:ok, %Mod{}}— same; the wrapper is stripped.{:error, _}— terminates the sequence cleanly (the property passes; an operation that refuses is not a contract violation).- Anything else raises an
ArgumentError; wrap your function or test it withcontract_holds/2.
The oracle is invariants and per-function contracts
The runner also checks each operation's own
@pre/@post/checkcontracts as it goes, so a struct module with per-function contracts but no@invariantis still meaningfully exercised. Invariants are the headline because they're what make the sequence form pull its weight — a module with no invariants buys little over testing each function withcontract_holds/2.
Options
:constructors(required, non-empty) — list of{fun_name, [arg_generators]}.:transformers(optional, default[]) — same shape; state threaded in as the first argument.:observers(optional, default[]) — same shape; state passed but not advanced.:name(optional) — a string used as the property's description. Defaults to"invariants_hold <module>".
Generates an ExUnit property that probes a function at its precondition boundaries: it mixes
the boundary values implied by the function's @pre (e.g. 0 and its neighbours for
@pre x >= 0) into your generators, discards any generated input that does not satisfy @pre,
and lets the function's own @post/check contracts be the oracle on the inputs that survive.
Pass a remote function capture and one base generator per argument:
probe_contract &Account.withdraw/2, args: [account_generator(), StreamData.integer()]How it differs from contract_holds/2:
- Boundary probing. Bond reads the function's
__bond_boundaries__/0reflection (emitted from the literal comparisons in its@pre) and blends each argument's boundary candidates into that argument's generator, so the edges — where off-by-one postcondition bugs live — are hit regularly rather than by chance. @preas a filter, not a guard. A generated input that violates the precondition is discarded — a generation miss, not a failure — instead of raising.contract_holds/2, by contrast, calls the function unconditionally and lets a@previolation fail the property. Reach forprobe_contract/2to generate broadly and probe boundaries; forcontract_holds/2when your generators already produce only valid inputs.
Because preconditions are the filter, the @post and check contracts are the oracle: any
postcondition violation on a valid input fails the property and StreamData shrinks to a minimal
counterexample.
Requirements and notes
- The capture must be a remote function (
&Module.fun/arity) — the contracts and the__bond_boundaries__/0/__bond_precondition__/3reflections live on that module. - Functions whose
@prehas no literal comparison (or no@preat all) are still exercised: there are simply no boundary candidates to inject and nothing to filter, soprobe_contractdegrades gracefully to plain generated testing. - If a single-clause function destructures an argument in its head (e.g.
def f(%Account{} = a, n)), your generator for that argument must produce shape-matching values, exactly as the function itself requires. - If the precondition is so restrictive that too many generated inputs are discarded,
StreamData raises its standard "too many filtered" error — narrow your base generators (or
use
StreamData.bind/2for relational preconditions) so they produce valid inputs more often.
Options
:args(required) — list ofStreamDatagenerators, one per function argument.:name(optional) — the property's description. Defaults to"probe_contract <source>".