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

danschultzer
In short Plug n’ play OAuth 2.0 provider library. Just set up a resource owner schema with Ecto (your user schema), install the dependen...
New
josevalim
Yes, yet another parser combinator library! Most of the parser combinators in the ecosystem are either compile-time, often using AST tra...
159 19951 141
New
tmbb
I’ve decided to create this topic to discuss optimization possibilities for something like Phoenix LiveView. I’ve created this topic unde...
144 10847 141
New
kip
ex_cldr provides localisation and internationalisation support based upon the data from the Unicode CLDR project. Unicode released CLDR ...
407 13366 120
New
fuelen
Hey folks! Want to present a toolkit for writing command-line user interfaces. It provides a convenient interface for colorizing text...
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
martinthenth
Hello everybody :wave: Recently, some of my colleagues talked about database ids and uuids and their problems, and I remembered the pain...
New

Other popular topics Top

baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 55125 245
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31586 112
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New

We're in Beta

About us Mission Statement