jvoegele

jvoegele

Bond (Design by Contract for Elixir) - 1.7.0 released: reusable contracts & contract-driven testing

Bond 1.7.0 is out. :tada: 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.
  • @pre as 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 @post as 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

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 @invariant never 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 shared Agent state, but which is race-free inside a serialized GenServer.

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

Where Next?

Popular in News & Updates Top

DominikWolek
After years of developing our framework, we have a release candidate for Membrane Core 1.0! :tada: The new version introduces children g...
New
DominikWolek
We have released Membrane Core v0.12.1 :tada::tada::tada: This release will help smooth the transition to the upcoming v1.0.0. See the r...
New
fhunleth
We are thrilled to announce another update for Nerves Systems to nerves_system_br 1.22.5-based releases. Nerves systems are the device su...
New
zachdaniel
Hey everyone! What I’ll cover in this post: Major refactors The future of Ash.Flow Current state of atomics and bulk actions Whats nex...
New
DominikWolek
Hey everyone! The Membrane Framework has a new website! It contains tutorials, blog posts, and other resources you’ll find handy while u...
New
DominikWolek
Hi everyone! After about 3 months of really hard work, we have released the very first version of Jellyfish Media Server! The best star...
New
mat-hek
Hi there! So far we’ve been posting news about Membrane &amp; multimedia mostly on X/Twitter, but from now on we’d like to share them on ...
New
zachdaniel
Ash 3.1 Released! Major themes Generators! These are just the first entries into a powerful new suite of tools. Check out the generator d...
New
jjcarstens
Testing code destined for hardware can be tricky, but it just got one tiny bit easier! :tty0tty was just released which is an Elixir por...
New
Jskalc
LiveVue v1.0 released After four release candidates and a lot of community feedback, LiveVue 1.0 is stable :tada: I’ve built a dedicated...
New

Other popular topics Top

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement