jvoegele
Bond (Design by Contract for Elixir) - 1.7.0 released: reusable contracts & contract-driven testing
Bond 1.7.0 is out.
It’s been four minor releases since I last posted here (1.3.0), and the theme of that run is reuse and testing: contracts you can name and share, contracts that quantify over collections, and — new in 1.7.0 — contracts that drive your property tests. Here’s the whole story since 1.3.0.
def deps do
[{:bond, "~> 1.7"}]
end
New to Bond? It brings Design by Contract to Elixir — @pre/@post/@invariant checked at runtime, with failure messages that tell you exactly what was violated and why. The original announcement and the getting-started guide are the best on-ramps.
Quantified assertions (1.4.0)
forall / exists let an assertion quantify over a collection, with comprehension-style generator syntax:
@pre all_positive: forall(x <- items, x > 0)
@post sorted: forall(i <- 0..(length(result) - 2)//1,
Enum.at(result, i) <= Enum.at(result, i + 1))
Unlike Enum.all?/2 / Enum.any?/2, a failure reports which element broke the predicate and its index — not just that the whole expression was false:
| assertion: forall(x <- items, x > 0)
| counterexample: element at index 3 (-2) does not satisfy `x > 0`
Both short-circuit, return ordinary booleans (so they compose with and/or/not/~>), and work in @pre, @post (including over result), @invariant, and Bond.check/1.
Reusable named contracts (1.5.0 + 1.6.0)
Inspired by discussion with @petr: declare a bundle of @pre/@post once, under a name, and apply it to many functions — in the same module or across modules.
defmodule Money do
use Bond
defcontract withdrawal(account, amount) do
@pre positive: amount > 0
@pre sufficient: amount <= account.balance
@post non_negative: result.balance >= 0
end
end
defmodule Account do
use Bond
@apply_contract {Money, :withdrawal}
def withdraw(acct, amt), do: %{acct | balance: acct.balance - amt}
end
The contract’s head supplies canonical argument names; an applying function’s parameters rebind to them positionally (so it can name them whatever it likes), exactly as an implementation inherits a behaviour callback’s contract. Violations are attributed to their source — precondition (from contract Money.withdrawal) failed … — with the originating {module, name} exposed on the error struct and in the [:bond, :assertion, :failure] telemetry.
1.6.0 then made named contracts composable. A function may add its own @pre/@post alongside @apply_contract, and a defcontract can include other contracts so small focused agreements build into larger ones:
defcontract positive(x), do: (@pre x > 0)
defcontract in_range(v, lo, hi), do: (@pre lo <= v and v <= hi)
defcontract order(item) do
include positive(item.quantity)
include in_range(item.discount, 0, 100)
@post priced: result.total >= 0
end
Each include argument is an expression over the host’s parameters, substituted into the included clauses (so error messages show the substituted form, e.g. item.quantity > 0). Includes nest transitively; a cycle is a compile error. Full rules and examples: the Reusable Contracts guide.
New in 1.7.0: contract-driven testing
A contract is a runtime predicate that fires at every call site — which makes it a ready-made test oracle. Bond has paired with StreamData since 1.0 (contract_holds/2, invariants_hold/2), and 1.7.0 adds probe_contract/2, which turns a function’s own @pre into the driver:
use Bond.PropertyTest
probe_contract &Account.withdraw/2, args: [account_gen(), StreamData.integer(0..100)]
Two things make it different from contract_holds/2:
- Boundary probing. Bond reads the literal comparisons in the function’s
@pre(e.g.amount >= 0,amount <= 100) at compile time and mixes the implied boundary values into your generators — so the property hits the edges, where off-by-one postcondition bugs live, deliberately rather than by chance. @preas a filter. An input that violates the precondition is discarded (a generation miss, not a failure) instead of failing the property — so you can generate broadly and still exercise only valid inputs, with the@postas the free oracle.
It’s inspired by Eiffel’s AutoTest, which likewise derives test cases from a routine’s contract and uses the contract itself as the pass/fail oracle. There’s a new Testing Contracts guide covering the whole testing surface — the example-based Bond.Test assertions and all three Bond.PropertyTest macros — and when to reach for each.
Stability
Everything since 1.3.0 is additive — no breaking changes — so each of these was a normal minor release and the new surface is covered by Bond’s stability guarantees. Compatibility is verified across Elixir 1.16–1.20 in CI.
Thanks & feedback
Named contracts came directly out of forum feedback, and the whole run was shaped by dogfooding these features in a real application — thank you. Questions, bug reports, and ideas are very welcome here or on the issue tracker.
Links
Most Liked
jvoegele
Bond 1.8.0 is out, adding Bond.Server — Design by Contract for GenServer process state.
Bond’s struct @invariant constrains every value of a type. Bond.Server constrains the state of a running process: use GenServer + use Bond.Server, and declare module-wide invariants that Bond checks around your callbacks.
defmodule Counter do
use GenServer
use Bond.Server
@state_invariant non_negative: state.count >= 0
@transition_invariant monotonic: new_state.count >= old_state.count
@impl true
def init(n), do: {:ok, %{count: n}}
@impl true
def handle_call(:inc, _from, state), do: {:reply, :ok, %{state | count: state.count + 1}}
end
@state_invariant— a property of the server’s state, checked after every state-transition callback returns. Because it wraps the callbacks themselves, it catches the common case of a callback mutating state inline ({:noreply, %{state | ...}}) — something a struct@invariantnever sees.@transition_invariant— relates the prior state (old_state) to the next (new_state) across every transition. “The counter never decreases” is exactly the kind of temporal property you can’t soundly assert over sharedAgentstate, but which is race-free inside a serializedGenServer.
Both reuse the existing :invariants configuration — the precondition ≤ postcondition ≤ invariant chain, Bond.Config runtime toggling, and :purge to compile them out — and a violation raises Bond.InvariantError, tagged by :kind.
{:bond, "~> 1.8"}
Docs
Popular in News & Updates
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex









