ArchTest.Assertions (ArchTest v0.2.0)

Copy Markdown View Source

Core assertion functions for architecture rules.

All assertion functions accept a ModuleSet as their first argument (supporting pipe-based DSL usage) and evaluate against the current dependency graph from ArchTest.Collector.

Violations cause an ExUnit.AssertionError with a detailed message listing all offending dependencies, grouped by module and annotated with the patterns that were checked.

Summary

Functions

Applies a custom check function to all modules in subject.

Asserts that there are 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 object is called by any module in callers.

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

Asserts that none of the modules matched by subject exist in the codebase.

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 that none of the modules in subject transitively depend on any module in object.

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

Asserts that only modules in allowed_callers may call modules in object.

Asserts that every module in subject only depends on modules in allowed.

Asserts that all modules in subject reside under the given namespace pattern.

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

Functions

assert_no_violations_public(violations, rule_name, context)

assert_no_violations_public(violations, rule_name, subject, object)

assert_no_violations_public(violations, rule_name, subject, object, user_message)

satisfying(subject, check_fn, opts \\ [])

@spec satisfying(
  ArchTest.ModuleSet.t(),
  (ArchTest.Collector.graph(), module() -> [ArchTest.Violation.t()]),
  keyword()
) :: :ok

Applies a custom check function to all modules in subject.

The function receives (graph, module) and must return a list of ArchTest.Violation structs (empty list = no violation).

Example

modules_matching("MyApp.**")
|> satisfying(fn graph, mod ->
  # custom check logic
  []
end)

should_be_free_of_cycles(subject, opts \\ [])

@spec should_be_free_of_cycles(
  ArchTest.ModuleSet.t(),
  keyword()
) :: :ok

Asserts that there are no circular dependencies among modules in subject.

Example

modules_matching("MyApp.Orders.**") |> should_be_free_of_cycles()

should_export(subject, fun_name, arity, opts \\ [])

@spec should_export(ArchTest.ModuleSet.t(), atom(), non_neg_integer(), keyword()) ::
  :ok

Asserts all modules in subject export the given function.

Example

modules_matching("**.*Handler")
|> should_export(:handle, 2)

modules_matching("MyApp.**.*")
|> should_export(:child_spec, 1)

should_have_attribute(subject, attr_key, opts \\ [])

@spec should_have_attribute(ArchTest.ModuleSet.t(), atom(), keyword()) :: :ok

Asserts all modules in subject have the given module attribute.

Checks mod.__info__(:attributes) for the presence of the key.

Example

# All plug modules must declare their behaviour
modules_matching("MyApp.Plugs.**")
|> should_have_attribute(:behaviour)

should_have_attribute_value(subject, attr_key, attr_value, opts \\ [])

@spec should_have_attribute_value(ArchTest.ModuleSet.t(), atom(), term(), keyword()) ::
  :ok

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

Example

modules_matching("**.*Schema")
|> should_have_attribute_value(:behaviour, [Ecto.Schema])

modules_matching("MyApp.**")
|> should_have_attribute_value(:moduledoc, false)
# (fails for modules with @moduledoc false -- use should_not_have_attribute_value instead)

should_have_module_count(subject, constraints)

@spec should_have_module_count(
  ArchTest.ModuleSet.t(),
  keyword()
) :: :ok

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

Supported constraint keys: :exactly, :at_least, :at_most, :less_than, :more_than. Multiple constraints can be combined.

Useful to enforce complexity budgets on bounded contexts, or to ensure a pattern is not accidentally empty (which would make rules trivially pass).

Options

In addition to constraint keys, the following options are supported:

  • :graph — a pre-built dependency graph map (useful for testing)
  • :app — OTP app atom for graph resolution (default: :all)
  • :message — custom hint appended to the error message on failure

Examples

# Context must not grow beyond 20 modules
modules_matching("MyApp.Orders.**")
|> should_have_module_count(less_than: 20)

# Pattern must match at least 1 module (catches typos)
modules_matching("MyApp.Orders.**")
|> should_have_module_count(at_least: 1)

# Range constraint
modules_matching("MyApp.Orders.**")
|> should_have_module_count(at_least: 2, at_most: 15)

should_have_name_matching(subject, name_pattern, opts \\ [])

@spec should_have_name_matching(ArchTest.ModuleSet.t(), String.t(), keyword()) :: :ok

Asserts that all modules in subject have names matching name_pattern.

Example

modules_matching("MyApp.Repo.**") |> should_have_name_matching("**.*Repo")

should_have_public_functions_matching(subject, pattern, opts \\ [])

@spec should_have_public_functions_matching(
  ArchTest.ModuleSet.t(),
  String.t(),
  keyword()
) :: :ok

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

Pattern is matched against the function name only (not arity).

Example

modules_matching("**.*Repo")
|> should_have_public_functions_matching("get*")

should_implement_behaviour(subject, behaviour, opts \\ [])

@spec should_implement_behaviour(ArchTest.ModuleSet.t(), module(), keyword()) :: :ok

Asserts that all modules in subject implement the given behaviour.

A module implements a behaviour if it declares @behaviour BehaviourModule, which appears in mod.__info__(:attributes) as behaviour: [BehaviourModule].

Example

modules_matching("**.*Handler")
|> should_implement_behaviour(MyApp.Handler)

modules_matching("**.*Server")
|> should_implement_behaviour(GenServer)

should_implement_protocol(subject, protocol, opts \\ [])

@spec should_implement_protocol(ArchTest.ModuleSet.t(), module(), keyword()) :: :ok

Asserts that all modules in subject implement the given protocol.

A module Mod implements protocol P if the module P.Mod is loadable (i.e. a defimpl P, for: Mod block exists somewhere in the codebase).

Example

modules_matching("**.*Entity")
|> should_implement_protocol(String.Chars)

should_not_be_called_by(object, callers, opts \\ [])

@spec should_not_be_called_by(
  ArchTest.ModuleSet.t(),
  ArchTest.ModuleSet.t(),
  keyword()
) :: :ok

Asserts that no module in object is called by any module in callers.

This is the reverse direction of should_not_depend_on.

Example

modules_matching("MyApp.Repo.*")
|> should_not_be_called_by(modules_matching("MyApp.Web.*"))

should_not_depend_on(subject, object, opts \\ [])

@spec should_not_depend_on(ArchTest.ModuleSet.t(), ArchTest.ModuleSet.t(), keyword()) ::
  :ok

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

Example

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

should_not_exist(subject, opts \\ [])

@spec should_not_exist(
  ArchTest.ModuleSet.t(),
  keyword()
) :: :ok

Asserts that none of the modules matched by subject exist in the codebase.

Used to ban naming conventions (e.g., no *Manager modules).

Example

modules_matching("**.*Manager") |> should_not_exist()

should_not_export(subject, fun_name, arity, opts \\ [])

@spec should_not_export(ArchTest.ModuleSet.t(), atom(), non_neg_integer(), keyword()) ::
  :ok

Asserts no module in subject exports the given function.

Example

modules_matching("MyApp.Domain.**")
|> should_not_export(:__struct__, 0)

modules_matching("**.*Controller")
|> should_not_export(:handle_info, 2)

should_not_have_attribute(subject, attr_key, opts \\ [])

@spec should_not_have_attribute(ArchTest.ModuleSet.t(), atom(), keyword()) :: :ok

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

A violation is raised for every module that DOES have the attribute key.

Example

modules_matching("MyApp.Web.**")
|> should_not_have_attribute(:deprecated)

should_not_have_attribute_value(subject, attr_key, attr_value, opts \\ [])

@spec should_not_have_attribute_value(
  ArchTest.ModuleSet.t(),
  atom(),
  term(),
  keyword()
) :: :ok

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

A violation is raised for every module whose attribute matches the forbidden value exactly.

Example

modules_matching("MyApp.**")
|> should_not_have_attribute_value(:moduledoc, false)

should_not_have_public_functions_matching(subject, pattern, opts \\ [])

@spec should_not_have_public_functions_matching(
  ArchTest.ModuleSet.t(),
  String.t(),
  keyword()
) :: :ok

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

Pattern is matched against the function name only.

Example

modules_matching("MyApp.Domain.**")
|> should_not_have_public_functions_matching("_*")

should_not_implement_behaviour(subject, behaviour, opts \\ [])

@spec should_not_implement_behaviour(ArchTest.ModuleSet.t(), module(), keyword()) ::
  :ok

Asserts that no module in subject implements the given behaviour.

This is the inverse of should_implement_behaviour/3 — a violation is raised for every module that DOES implement the behaviour.

Example

modules_matching("**.*Worker")
|> should_not_implement_behaviour(GenServer)

should_not_implement_protocol(subject, protocol, opts \\ [])

@spec should_not_implement_protocol(ArchTest.ModuleSet.t(), module(), keyword()) ::
  :ok

Asserts that no module in subject implements the given protocol.

This is the inverse of should_implement_protocol/3 — a violation is raised for every module that DOES implement the protocol.

Example

modules_matching("**.*Internal")
|> should_not_implement_protocol(Jason.Encoder)

should_not_transitively_depend_on(subject, object, opts \\ [])

@spec should_not_transitively_depend_on(
  ArchTest.ModuleSet.t(),
  ArchTest.ModuleSet.t(),
  keyword()
) ::
  :ok

Asserts that none of the modules in subject transitively depend on any module in object.

Example

modules_matching("MyApp.Orders.*")
|> should_not_transitively_depend_on(modules_matching("MyApp.Billing.*"))

should_not_use(subject, used_module, opts \\ [])

@spec should_not_use(ArchTest.ModuleSet.t(), module(), keyword()) :: :ok

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

This is the inverse of should_use/3 -- a violation is raised for every module that DOES appear to use the given module.

Example

modules_matching("MyApp.Web.**")
|> should_not_use(GenServer)

should_only_be_called_by(object, allowed_callers, opts \\ [])

@spec should_only_be_called_by(
  ArchTest.ModuleSet.t(),
  ArchTest.ModuleSet.t(),
  keyword()
) :: :ok

Asserts that only modules in allowed_callers may call modules in object.

Any module outside allowed_callers that calls a module in object is a violation. This is the whitelist form of should_not_be_called_by/3.

Example

# Only Services and Controllers may call Repo modules
modules_matching("**.*Repo")
|> should_only_be_called_by(
     modules_matching("**.*Service")
     |> union(modules_matching("**.*Controller"))
   )

# The Accounts module may only be called by the Orders context
modules_matching("MyApp.Accounts")
|> should_only_be_called_by(modules_matching("MyApp.Orders.**"),
     message: "Use the Orders public API to access Accounts — see ADR-012")

should_only_depend_on(subject, allowed, opts \\ [])

@spec should_only_depend_on(ArchTest.ModuleSet.t(), ArchTest.ModuleSet.t(), keyword()) ::
  :ok

Asserts that every module in subject only depends on modules in allowed.

Any dependency outside allowed is a violation.

Example

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

should_reside_under(subject, namespace_pattern, opts \\ [])

@spec should_reside_under(ArchTest.ModuleSet.t(), String.t(), keyword()) :: :ok

Asserts that all modules in subject reside under the given namespace pattern.

Example

modules_matching("**.*Schema") |> should_reside_under("MyApp.**.Schemas")

should_use(subject, used_module, opts \\ [])

@spec should_use(ArchTest.ModuleSet.t(), module(), keyword()) :: :ok

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

This is detected heuristically: when a module does use Foo, Elixir typically adds :behaviour or other attributes. This check looks for module appearing anywhere in the module's flattened attribute values.

For reliable results with framework modules (GenServer, Ecto.Schema etc.), prefer should_implement_behaviour/2 instead.

Example

modules_matching("**.*Schema")
|> should_use(Ecto.Schema)