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
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.
I cannot quickly guess from the front page: are those test blocks kind of like an extension of a standard ExUnit.Case? Or are they an actual test file and assertions should go below the f.ex. modules_matching("MyApp.**.*Service")? Seems to be the latter but I want to make sure.
Thanks! That’s exactly the use case the freeze mechanism was designed for — snapshot current violations, only fail on newly introduced ones as you refactor. Let existing tech debt be,
enforce the new direction going forward.
The Freezing guide walks through the full workflow if useful.
Yes, plain ExUnit test files — use ArchTest just imports the DSL macros. Each test block is a standard ExUnit.Case test, and the assertions raise if violated:
defmodule MyApp.ArchitectureTest 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
end
Nothing special — just ExUnit. The pipe is the assertion itself. The Getting Started guide has the full setup walkthrough.
Very nice ! I just spent a bit of the last week introducing this same idea to some of my applications. Boundary is great too but does not work on this exact problem.
In my case I chose to write a basic elixir script that I added to my CI and work with text and allowlists, with a warning and error level. Text can seem naïve (and it is) but this allowed me to surface similarities in naming in various parts of the codebase that made me think “huh, I wouldn’t want to embark a developer with those 3 things having such similar names” and made that an error.
It helps me in refactors by making the “before” state a CI and lint error, and fixing occurences one by one. I think I should migrate to your tool.