yoavgeva

yoavgeva

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!

First Post!

felix-starman

felix-starman

I’m excited to put this in a couple apps I work on that are older and perpetually are in the middle of removing an old pattern slowly

Most Liked

yoavgeva

yoavgeva

Great suggestion @felix-starman — shipped in v0.2.0 :tada:

mix igniter.install arch_test # basic cycle-check file
mix arch_test.gen.phoenix # Phoenix layers + naming + conventions
mix arch_test.gen.layers # web → context → repo
mix arch_test.gen.onion # domain → application → adapters → web
mix arch_test.gen.modulith # bounded-context isolation
mix arch_test.gen.naming # no Managers, schema placement
mix arch_test.gen.conventions # no IO.puts, dbg, bare raise
mix arch_test.gen.freeze # baseline existing violations

Generated files are plain ExUnit tests — edit to fit your namespaces and delete what doesn’t apply.

More information in arch_test/README.md at main · yoavgeva/arch_test · GitHub

Add {:igniter, “~> 0.7”, only: [:dev, :test], runtime: false} and you’re set.

Lucassifoni

Lucassifoni

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.

yoavgeva

yoavgeva

Compiler does catch circular dependencies like → mutal import, use, some struct references, etc.
that create a compile order deadlock.

ArchTest catches with should_be_free_of_cycles → Runtime call graph cycles, these are cases where module A calls a function in B, and B calls a function in A.
Elixir compiles these just fine — there’s no compilation deadlock — but they represent tight architectural coupling you might want to prevent.

Example: test fixture and assertion tests

ArchTest isn’t about catching things that won’t compile — it’s about enforcing the architectural rules your team has agreed on, so they don’t quietly erode over time. Things like “the Web layer
should never call into Orders directly”, “no circular dependencies between bounded contexts”, or “no IO.puts left in production code”. The code will compile and run fine without these rules — but six months later you’ve got a ball of mud.
ArchTest makes those rules fail the test suite, so they stay enforced as the codebase grows and new people join the team.

Last Post!

yoavgeva

yoavgeva

Small update: ArchTest 0.3.x is ready.

Main things added in 0.3.x:

  • use ArchTest, app: :my_app for app-scoped architecture tests
  • empty rule subjects now fail by default, so typoed patterns dont silently pass
  • freeze: true for easier gradual adoption
  • ArchTest.Collector.calls/2 for function-level call metadata
  • ArchTest.Rule for reusable rules with ignore/freeze support
  • define_slices_by/2 for auto-discovering modulith slices
  • ArchTest.PlantUML.enforce/2 to check component diagrams against real dependencies
  • extra metrics like afferent/2, efferent/2, fan_in/2, fan_out/2, dependency_depth/2
  • better Igniter generators with app scoping and safer defaults

0.3.1 also includes executable examples against a real compiled fixture app, including PlantUML, call metadata, reusable rules, captured slices, and metrics. I added
this becuase docs are nice, but seeing it run against real BEAM files is alot clearer.

Upgrade:

{:arch_test, "~> 0.3", only: :test, runtime: false}

Migration guide from 0.2:
https://hexdocs.pm/arch_test/migrating-to-0.3.html

Docs:
ArchTest v0.3.1 — Documentation

Hex:
arch_test | Hex

Thanks again for the feedback here, it helped shape this release quite a bit.

Where Next?

Popular in Announcing Top

mischov
import Meeseeks.CSS html = HTTPoison.get!("https://news.ycombinator.com/").body for story <- Meeseeks.all(html, css("tr.athing")) do...
New
Azolo
Hey everyone, I just released WebSockex which is a Elixir WebSocket client. WebSockex strives to work as a OTP special process, be RFC6...
New
pkrawat1
Hey guyz We at @aviabird are working on a payment library in elixir/phoenix. We are targeting March 2018 to add 56 Gateways to it. Have...
New
wfgilman
I’ve cleaned up and open sourced three financial libraries I was using for my company. They are bindings for the APIs of these three comp...
New
RobertDober
Earmark is a pure-Elixir Markdown converter. It is intended to be used as a library (just call Earmark.as_html), but can also be used as...
239 12978 134
New
maltoe
Hello! Came here to announce ChromicPDF, a pet project PDF generator I’ve been working on for the past few months. Why another PDF gener...
New
restlessronin
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub. Docs are at OpenaiEx User Gu...
152 10796 134
New

Other popular topics Top

New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 42716 114
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

We're in Beta

About us Mission Statement