F5.07 · part 3 of 3
Contract tests
The contract from F5.04 was three promises about a command: a precondition the
caller must meet, a postcondition the command guarantees, and an invariant
always true of the state. Written as documentation they describe behaviour; written as tests they enforce it. A
precondition test feeds a bad command and asserts {:error, reason} with no state change;
a postcondition test runs a valid command and asserts the guarantee; an invariant test asserts the rule on the
resulting state. And doctest runs the @doc examples as tests, so
the documentation cannot drift from the code. The contract stops being a comment and becomes something the suite
checks.
Three promises, three assertions. Select a term to see what its test feeds in and checks.
A contract you can run
Each clause of the contract maps to an assertion. The precondition becomes a refused bad input; the postcondition becomes a checked guarantee; the invariant becomes a property of the result. Doctests cover the examples in between. Select a term.
Contract term as a test · select one
term: precondition
the test asserts: bad input returns {:error, _}
A contract in a comment is a hope; a contract in a test is a guarantee. The same three terms from F5.04, run on every commit, keep the engine honest.
In code
The contract terms as assertions, and a one-line doctest. The precondition test
refuses a second enrollment; the invariant test records progress and checks the bound; and
doctest runs every @doc example in the engine module so the
documentation stays true.
# the F5.04 contract, now run as tests test "enrolling twice is refused (precondition)" do state = Engine.evolve(%LearnerEnrolled{user_id: "u1"}, State.new()) assert {:error, :already_enrolled} = Engine.decide(state, {:enroll, "u1", "c1"}) end test "recording progress keeps it in 0..100 (invariant)" do {:ok, [event]} = Engine.decide(state, {:record_progress, "e1", 40}) assert State.progress(Engine.evolve(event, state), "e1") in 0..100 end doctest Portal.Engine # the @doc examples run as tests, so the docs cannot drift
contract to assertion
Precondition, postcondition, invariant — each a line the suite checks, not prose.
docs that run
doctest runs the @doc examples, so they cannot go stale.
That closes F5.07: the pure core is tested by example, properties guard the invariants over generated inputs, and the F5.04 contract runs as tests. The next module, F5.08 — Boundaries & integration seams, wraps the engine behind ports and a facade with error shapes the UI can render. Back to the module overview or the chapter.
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.