View Source Getting Started

This guide walks you through adding Bond to a project, writing your first contract, and the most common patterns you'll encounter.

For full reference material see the Bond module docs.

Installation

Add bond to your dependencies in mix.exs:

def deps do
  [
    {:bond, "~> 0.14.0"}
  ]
end

Then run mix deps.get.

Your first contract

use Bond in any module to enable @pre, @post, and check/1,2. Add a precondition before a function definition:

defmodule Calculator do
  use Bond

  @pre is_number(x)
  def square(x), do: x * x
end

Calculator.square(2) returns 4 as expected. Calculator.square("two") raises Bond.PreconditionError with a message that points at the failing assertion:

** (Bond.PreconditionError) precondition failed for call to Calculator.square/1
|   at: lib/calculator.ex:4
|   label: nil
|   assertion: is_number(x)
|   binding: [x: "two"]

Adding a postcondition

Postconditions are evaluated after the function body. They have access to the function's parameters plus a result variable bound to the return value:

defmodule Account do
  use Bond

  @pre amount > 0
  @post result >= 0
  def withdraw(balance, amount), do: balance - amount
end

Account.withdraw(100, 30) returns 70. Account.withdraw(20, 50) raises Bond.PostconditionError — the function returned a negative balance, which the postcondition forbids.

Labelled assertions

A single @pre or @post may contain multiple labelled assertions as a keyword list. Labels appear in error messages so it's easy to identify which assertion failed:

defmodule Math do
  use Bond

  @pre numeric_x: is_number(x), non_negative_x: x >= 0
  @post float_result: is_float(result),
        non_negative_result: result >= 0.0
  def sqrt(x), do: :math.sqrt(x)
end

Labels can be atoms (when they're valid Elixir identifiers) or strings (for phrases with spaces or punctuation):

@post "result is integer or zero": is_integer(result) or result == 0

Predicates and operators

The Bond.Predicates module is automatically imported inside assertion expressions. Two operators are especially useful in contracts:

  • ~> — logical implication. (p ~> q) means "if p then q".
  • <~ — pattern match. (pattern <~ expression) is match?(pattern, expression).
@post "sqrt of 0 is 0": (x == 0) ~> (result === 0.0)
@post {:ok, _} <~ result

See Bond.Predicates for the complete list.

old expressions in postconditions

For functions that mutate state, a postcondition often needs to compare the new state to the old state. The old/1 macro snapshots a value before the function body runs:

defmodule Counter do
  use Bond
  use Agent

  def start_link(initial), do: Agent.start_link(fn -> initial end)
  def get(agent), do: Agent.get(agent, & &1)

  @post incremented: get(agent) == old(get(agent)) + 1
  def increment(agent), do: Agent.update(agent, &(&1 + 1))
end

See the Contracts in a Concurrent World guide for the subtleties of using old with stateful, concurrent code.

Inline checks

For sanity checks inside a function body, use check/1 or check/2:

def total(items) do
  raw = Enum.sum(items)

  check raw >= 0
  check "total is integer", is_integer(raw)

  raw
end

check is for development confidence, not validation

Don't use check for input validation or anything else that protects the integrity of your code — it can be compiled out entirely (see below).

Disabling contracts in production

Bond's three application-config keys — :preconditions, :postconditions, :checks — each accept true, false, or :purge:

# config/prod.exs — strip contracts entirely from the prod build
config :bond,
  preconditions: :purge,
  postconditions: :purge,
  checks: :purge
  • true (default) — compiled in, runtime-togglable, evaluated by default.
  • false — compiled in, runtime-togglable, not evaluated by default.
  • :purge — not compiled at all. Zero overhead. No contract docs.

When compiled with true or false, contracts can be flipped at runtime via Application.put_env(:bond, :preconditions, false | true) — no recompilation needed. :purge is the only setting with no runtime presence (the code isn't there).

For finer control, the :overrides config lets you set per-module rules. See the Bond moduledoc's "Conditional compilation" and "Per-module overrides" sections for the full story.

Testing contract violations

For testing that a contract IS raised (or that a specific contract isn't), Bond.Test provides ExUnit helpers:

defmodule MyAppTest do
  use ExUnit.Case
  use Bond.Test

  alias MyApp.Math

  test "sqrt rejects negative input" do
    assert_precondition_violation(Math.sqrt(-1), label: :non_negative_x)
  end
end

See Bond.Test for assert_precondition_violation/2, assert_postcondition_violation/2, and assert_check_violation/2.

Next steps

  • The Bond moduledoc has the full reference, including the Invariants section if you have a struct module and want module-wide constraints on every instance, and the Property-based testing section for using contracts as oracles with StreamData.
  • The Contracts in a Concurrent World guide covers old, race conditions, how to design contracts for stateful processes, and how @invariant strengthens the pure-state-struct pattern.
  • The FAQ answers common questions: "why contracts when I have ExUnit?", "how does Bond compare to Norm?", "when does Bond check invariants?", "how does Bond compose with StreamData?", and so on.