assert_boundary

CIHex.pmDocs

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}
  ]
end

Usage

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):

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