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"}
]
endThen 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
endCalculator.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
endAccount.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)
endLabels 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 == 0Predicates and operators
The Bond.Predicates module is automatically imported inside assertion
expressions. Two operators are especially useful in contracts:
~>— logical implication.(p ~> q)means "ifpthenq".<~— pattern match.(pattern <~ expression)ismatch?(pattern, expression).
@post "sqrt of 0 is 0": (x == 0) ~> (result === 0.0)
@post {:ok, _} <~ resultSee 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))
endSee 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
checkis for development confidence, not validationDon't use
checkfor 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: :purgetrue(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
endSee Bond.Test for assert_precondition_violation/2,
assert_postcondition_violation/2, and assert_check_violation/2.
Next steps
- The
Bondmoduledoc 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@invariantstrengthens 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.