ArchTest - Architecture rules as tests. Enforced from bytecode

ArchTest – Architecture rules as tests. Enforced from bytecode.

ArchTest is an ArchUnit-inspired architecture testing library for Elixir. Rules live in plain ExUnit tests, run with mix test, and produce structured failure output listing every violation — with zero changes to your production code.

The problem it solves

Elixir has great tools, but there’s a gap:

  • Credo catches style issues and code smells within a file — it won’t tell you your domain layer is calling your web layer

  • Boundary gives compile-time warnings for declared boundaries, but requires annotating every module and can’t easily express transitive rules or glob-based selections

  • ArchTest fills the rest: dependency direction, transitive paths, cycle detection, naming conventions, coupling metrics — all in ExUnit, no production code touched

What it does

Write architecture rules as regular tests:

elixir

defmodule MyApp.ArchTest do
  use ExUnit.Case
  use ArchTest

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

  test "no circular dependencies" do
    modules_matching("MyApp.**") |> should_be_free_of_cycles()
  end

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

Key features

  • Dependency assertions: should_not_depend_on, should_only_depend_on, should_not_be_called_by, should_only_be_called_by, should_not_transitively_depend_on, should_be_free_of_cycles

  • Layered, onion/hexagonal, and modulith/bounded-context architecture enforcement out of the box

  • Flexible module selection with glob patterns, excluding, union, intersection, and modules_satisfying/1 for custom predicates

  • Code conventions: ban IO.puts, dbg, bare raise, undocumented public functions, and more

  • Coupling metrics: instability, abstractness, distance from the main sequence (Martin metrics)

  • Violation freeze for gradual adoption — baseline existing violations and only fail on new ones

Motivation

I built this for my own projects after wanting ArchUnit-style rules in Elixir and finding no equivalent. It works from compiled bytecode, so there’s nothing to annotate and no build step to change.

Links

https://github.com/yoavgeva/arch_test

Would love any feedback, especially on the DSL ergonomics and any rule types you’d find useful!

13 Likes