assert_boundary
ExUnit assertion helpers for testing module dependency boundaries.
Uses Erlang's :xref to analyze BEAM bytecode and verify that module
dependencies respect architectural boundaries. Encode your architecture as
tests — they fail the moment someone introduces a forbidden dependency.
Installation
def deps do
[
{:assert_boundary, "~> 0.1.0", only: :test}
]
endUsage
defmodule MyApp.BoundaryTest do
use ExUnit.Case, async: true
use AssertBoundary, app: :my_app
test "web layer doesn't touch repo internals", %{boundary: boundary} do
refute_calls(boundary, from: under(MyApp.Web), to: under(MyApp.Repo.Internal))
end
test "domain has restricted dependencies", %{boundary: boundary} do
assert_boundary(boundary,
modules: under(MyApp.Domain),
allow: [under(MyApp.Schema), under(MyApp.Types)]
)
end
test "controllers depend on domain", %{boundary: boundary} do
assert_calls(boundary, from: under(MyApp.Web.Controller), to: under(MyApp.Domain))
end
test "repo internals are encapsulated", %{boundary: boundary} do
assert_encapsulated(boundary,
modules: under(MyApp.Repo.Internal),
allow: [under(MyApp.Repo)]
)
end
endAPI
refute_calls(graph, from: pattern, to: pattern)
Asserts that no module matching from calls any module matching to. Use
this to enforce that two parts of your system don't directly depend on each
other.
assert_calls(graph, from: pattern, to: pattern)
Asserts that at least one module matching from calls a module matching to.
Use this to verify expected dependencies exist.
assert_boundary(graph, modules: pattern, allow: [pattern])
Asserts that modules matching modules only depend on modules matching the
allow patterns. Dependencies within the boundary are always permitted.
This is an allowlist — any dependency not explicitly allowed is a violation.
assert_encapsulated(graph, modules: pattern, allow: [pattern])
Asserts that modules matching modules are only called by modules matching
the allow patterns. Calls from within the boundary are always permitted.
This is the inverse of assert_boundary — it constrains incoming callers
rather than outgoing dependencies.
Patterns
All assertion functions accept patterns that match module names (without
the Elixir. prefix):
- Regex:
~r/^MyApp\.Domain/ - Module atom:
MyApp.Domain.User - Prefix:
under(MyApp.Domain)— matches the module and all children - List:
[~r/^MyApp\.Domain/, MyApp.Shared.Types]
Graph introspection
graph = AssertBoundary.graph(:my_app)
# List all modules matching a pattern.
AssertBoundary.Graph.matching(graph, ~r/^MyApp\.Web/)
# List direct dependencies of a specific module.
AssertBoundary.Graph.dependencies_of(graph, MyApp.Web.Router)How it works
AssertBoundary analyzes compiled BEAM files using Erlang's :xref module.
The dependency graph reflects actual call dependencies from bytecode — not
source-level references like alias or import. Only calls between modules
within the same application are tracked; calls to external libraries are
excluded.
The graph is built once per test module via setup_all and passed through
ExUnit's test context as %{boundary: graph}.
License
MIT