# assert_boundary
[](https://github.com/QuinnWilton/assert_boundary/actions/workflows/ci.yml)
[](https://hex.pm/packages/assert_boundary)
[](https://hexdocs.pm/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
```elixir
def deps do
[
{:assert_boundary, "~> 0.1.0", only: :test}
]
end
```
## Usage
```elixir
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
end
```
## API
### `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
```elixir
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