jonnifyelixir
F5 / F5.07 / property

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

DETERMINISM replay(log) == replay(log) · for any generated log gen: event logs INVARIANT progress in 0..100 · after any command sequence gen: commands TOTALITY decide → {:ok, _} | {:error, _} · never raises gen: valid commands

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

Related in this course