ArchTest (ArchTest v0.2.0)

Copy Markdown View Source

ArchUnit-inspired architecture testing library for Elixir.

Write ExUnit tests that enforce architectural rules — dependency direction, layer boundaries, bounded-context isolation, and naming conventions — using a fluent, pipe-based DSL.

Quick start

defmodule MyApp.ArchitectureTest do
  use ExUnit.Case
  use ArchTest

  test "service modules don't call repos directly" do
    modules_matching("**.*Service")
    |> should_not_depend_on(modules_matching("**.*Repo"))
  end

  test "no Manager modules exist" do
    modules_matching("MyApp.**.*Manager") |> should_not_exist()
  end
end

Options for use ArchTest

  • :app — OTP app atom to limit introspection (default: :all)
  • :freezetrue to auto-freeze all rules in the module (default: false)

Module reference

Summary

Functions

Returns a ModuleSet matching every module in the application.

Asserts every module under namespace_pattern belongs to a declared slice.

Asserts every module under namespace_pattern belongs to a declared slice.

Allows from_slice to call the public API of to_slice.

Defines an ordered list of architecture layers (top to bottom).

Defines an onion/hexagonal architecture (innermost layer first).

Defines bounded-context slices for a modulith architecture.

Enforces layer direction (each layer may only depend on layers below it).

Enforces bounded-context isolation (see ArchTest.Modulith).

Enforces onion architecture rules (dependencies point only inward).

Excludes modules matching pattern from a ModuleSet.

Returns modules present in both ModuleSets (intersection / AND).

Returns a ModuleSet for all direct children of namespace.

Returns a ModuleSet for modules matching the given glob pattern.

Returns a ModuleSet matching modules that satisfy a custom predicate.

Applies a custom check function (graph, module -> [Violation.t()]) to each module in subject.

Asserts no circular dependencies among modules in subject.

Asserts all modules in subject export the given function.

Asserts all modules in subject have the given module attribute.

Asserts all modules in subject have the given attribute with the given value.

Asserts that the number of modules matching subject satisfies the given constraints.

Asserts that all modules in subject have names matching name_pattern.

Asserts all modules in subject have at least one public function whose name matches the given glob pattern.

Asserts that all modules in subject implement the given behaviour.

Asserts that all modules in subject implement the given protocol.

Asserts that no module in callers calls any module in object.

Asserts that no module in subject directly depends on modules in object.

Asserts that slices have absolutely no cross-slice dependencies (strict isolation).

Asserts that no module in subject exists.

Asserts no module in subject exports the given function.

Asserts all modules in subject do NOT have the given module attribute.

Asserts all modules in subject do NOT have the given attribute with the given value.

Asserts no module in subject has public functions whose names match the pattern.

Asserts that no module in subject implements the given behaviour.

Asserts that no module in subject implements the given protocol.

Asserts no transitive dependency from subject to modules in object.

Asserts no module in subject uses the given module (via use ModuleName).

Asserts only modules in allowed_callers may call modules in object.

Asserts that modules in subject only depend on modules in allowed.

Asserts that all modules in subject reside under namespace_pattern.

Asserts all modules in subject use the given module (via use ModuleName).

Combines two ModuleSets (union / OR).

Functions

all_modules()

@spec all_modules() :: ArchTest.ModuleSet.t()

Returns a ModuleSet matching every module in the application.

all_modules_covered_by(modulith, namespace_pattern)

Asserts every module under namespace_pattern belongs to a declared slice.

all_modules_covered_by(modulith, namespace_pattern, opts)

Asserts every module under namespace_pattern belongs to a declared slice.

allow_dependency(modulith, from_slice, to_slice)

@spec allow_dependency(ArchTest.Modulith.t(), atom(), atom()) :: ArchTest.Modulith.t()

Allows from_slice to call the public API of to_slice.

define_layers(layer_defs)

@spec define_layers(keyword()) :: ArchTest.Layers.t()

Defines an ordered list of architecture layers (top to bottom).

Example

define_layers(
  web:     "MyApp.Web.**",
  context: "MyApp.**",
  repo:    "MyApp.Repo.**"
)
|> enforce_direction()

define_onion(layer_defs)

@spec define_onion(keyword()) :: ArchTest.Layers.t()

Defines an onion/hexagonal architecture (innermost layer first).

Example

define_onion(
  domain:      "MyApp.Domain.**",
  application: "MyApp.Application.**",
  adapters:    "MyApp.Adapters.**"
)
|> enforce_onion_rules()

define_slices(slice_defs)

@spec define_slices(keyword()) :: ArchTest.Modulith.t()

Defines bounded-context slices for a modulith architecture.

Example

define_slices(
  orders:    "MyApp.Orders",
  inventory: "MyApp.Inventory",
  accounts:  "MyApp.Accounts"
)
|> allow_dependency(:orders, :accounts)
|> enforce_isolation()

enforce_direction(layers)

@spec enforce_direction(ArchTest.Layers.t()) :: :ok

Enforces layer direction (each layer may only depend on layers below it).

enforce_isolation(modulith)

@spec enforce_isolation(ArchTest.Modulith.t()) :: :ok

Enforces bounded-context isolation (see ArchTest.Modulith).

enforce_onion_rules(layers)

@spec enforce_onion_rules(ArchTest.Layers.t()) :: :ok

Enforces onion architecture rules (dependencies point only inward).

excluding(module_set, pattern)

Excludes modules matching pattern from a ModuleSet.

Example

modules_matching("MyApp.**")
|> excluding("MyApp.Test.*")

intersection(a, b)

Returns modules present in both ModuleSets (intersection / AND).

modules_in(namespace)

@spec modules_in(String.t()) :: ArchTest.ModuleSet.t()

Returns a ModuleSet for all direct children of namespace.

Shorthand for modules_matching("Namespace.*").

Example

modules_in("MyApp.Orders")
# equivalent to modules_matching("MyApp.Orders.*")

modules_matching(pattern)

@spec modules_matching(String.t()) :: ArchTest.ModuleSet.t()

Returns a ModuleSet for modules matching the given glob pattern.

Pattern semantics

PatternMatches
"MyApp.Orders.*"Direct children only
"MyApp.Orders.**"All descendants at any depth
"MyApp.Orders"Exact match only
"**.*Service"Last segment ends with Service
"**.*Service*"Last segment contains Service

Example

modules_matching("**.*Controller")
|> should_not_depend_on(modules_matching("**.*Repo"))

modules_satisfying(filter_fn)

@spec modules_satisfying((module() -> boolean())) :: ArchTest.ModuleSet.t()

Returns a ModuleSet matching modules that satisfy a custom predicate.

Example

modules_satisfying(fn mod ->
  function_exported?(mod, :__schema__, 1)
end)
|> should_reside_under("MyApp.**.Schemas")

satisfying(subject, check_fn)

Applies a custom check function (graph, module -> [Violation.t()]) to each module in subject.

should_be_free_of_cycles(subject)

Asserts no circular dependencies among modules in subject.

should_export(subject, fun_name, arity)

Asserts all modules in subject export the given function.

should_have_attribute(subject, attr_key)

Asserts all modules in subject have the given module attribute.

should_have_attribute_value(subject, attr_key, attr_value)

Asserts all modules in subject have the given attribute with the given value.

should_have_module_count(subject, constraints)

Asserts that the number of modules matching subject satisfies the given constraints.

should_have_name_matching(subject, name_pattern)

Asserts that all modules in subject have names matching name_pattern.

should_have_public_functions_matching(subject, pattern)

Asserts all modules in subject have at least one public function whose name matches the given glob pattern.

should_implement_behaviour(subject, behaviour)

Asserts that all modules in subject implement the given behaviour.

should_implement_protocol(subject, protocol)

Asserts that all modules in subject implement the given protocol.

should_not_be_called_by(object, callers)

Asserts that no module in callers calls any module in object.

should_not_depend_on(subject, object)

Asserts that no module in subject directly depends on modules in object.

should_not_depend_on_each_other(modulith)

@spec should_not_depend_on_each_other(ArchTest.Modulith.t()) :: :ok

Asserts that slices have absolutely no cross-slice dependencies (strict isolation).

should_not_exist(subject)

Asserts that no module in subject exists.

should_not_export(subject, fun_name, arity)

Asserts no module in subject exports the given function.

should_not_have_attribute(subject, attr_key)

Asserts all modules in subject do NOT have the given module attribute.

should_not_have_attribute_value(subject, attr_key, attr_value)

Asserts all modules in subject do NOT have the given attribute with the given value.

should_not_have_public_functions_matching(subject, pattern)

Asserts no module in subject has public functions whose names match the pattern.

should_not_implement_behaviour(subject, behaviour)

Asserts that no module in subject implements the given behaviour.

should_not_implement_protocol(subject, protocol)

Asserts that no module in subject implements the given protocol.

should_not_transitively_depend_on(subject, object)

Asserts no transitive dependency from subject to modules in object.

should_not_use(subject, used_module)

Asserts no module in subject uses the given module (via use ModuleName).

should_only_be_called_by(object, allowed_callers)

Asserts only modules in allowed_callers may call modules in object.

should_only_depend_on(subject, allowed)

Asserts that modules in subject only depend on modules in allowed.

should_reside_under(subject, namespace_pattern)

Asserts that all modules in subject reside under namespace_pattern.

should_use(subject, used_module)

Asserts all modules in subject use the given module (via use ModuleName).

union(a, b)

Combines two ModuleSets (union / OR).

Example

modules_matching("**.*Service")
|> union(modules_matching("**.*View"))