F5.07 · part 2 of 3
Property-based testing
An example test checks the cases you thought of; a property test checks a rule over the cases the
machine thinks of. You state a property — something that must hold for every valid input
— and StreamData generates hundreds of inputs trying to find one that breaks it,
shrinking any failure to the smallest counterexample. The engine offers natural properties: replaying a log is
deterministic, the F5.04 invariant (progress in 0..100)
survives any sequence of valid commands, and decide is total on valid
input — it returns a tagged result rather than raising. These hold for the whole input space, not three
hand-picked points.
Three properties of the engine. Select one to see the rule and what the generator throws at it.
Rules, not examples
A property names something that should always be true and quantifies it over generated inputs. The generator produces random-but-valid event logs and command sequences; the property asserts the rule on each. When it fails, the framework shrinks the input to the smallest case that still breaks — the counterexample you actually want. Select a property.
Engine property · select one
property: determinism
the rule: same log folds to the same state
An example proves a point; a property states a law. When the architecture is pure and event-sourced, the laws are easy to name — and a generator is merciless about finding the input that violates one.
In code
Two properties with ExUnitProperties. The first folds a generated log twice and
asserts the two states match; the second runs a generated command sequence and asserts the invariant on the
result. check all draws from the generators and shrinks on failure.
use ExUnitProperties property "folding a log is deterministic" do check all log <- list_of(event_generator()) do assert Engine.replay(log) == Engine.replay(log) end end property "progress stays within 0..100" do check all cmds <- list_of(command_generator()) do state = Enum.reduce(cmds, State.new(), &apply_command/2) assert Enum.all?(State.progresses(state), &(&1 in 0..100)) end end
state the law
Determinism, an invariant, totality — rules that hold for every valid input.
let it generate
check all draws hundreds of cases and shrinks any failure to its smallest form.
Next: contract tests — the F5.04 precondition, postcondition, and invariant written as assertions, plus doctests on the docs.
References
Primary sources for this lesson, and where it connects in the course.
Sources
- Elixir — ExUnit — the test framework.
- StreamData — property-based testing of the pure core.
- Elixir — ExUnit.DocTest — examples in @doc become tests.