Skip to content

Architecture Tests with ArchUnitNET

Architecture tests turn structural conventions into failing builds. They live in a dedicated project (for example MyApp.Architecture.Tests) and use ArchUnitNET.

What to enforce

  • Reference direction
  • Domain isolation
  • Naming conventions
  • One class per file

Loading the architecture

internal static class ArchitectureFixture
{
    public static readonly Architecture Architecture = new ArchLoader()
        .LoadAssemblies(
            typeof(MyApp.Core.CoreMarker).Assembly,
            typeof(MyApp.Search.SearchMarker).Assembly,
            typeof(MyApp.Download.DownloadMarker).Assembly)
        .Build();
}

Reference direction

The allowed direction is Host -> API -> Domains -> Core -> Messages/Persistence. Never upward.

[Fact]
public void Core_DoesNotDependOnDomains()
{
    var core = Types().That().ResideInNamespaceMatching(@"MyApp\.Core(\..*)?").As("Core");
    var domains = Types().That().ResideInNamespaceMatching(@"MyApp\.(Search|Download)(\..*)?");

    core.Should().NotDependOnAny(domains).Check(ArchitectureFixture.Architecture);
}

Domain isolation

Domain projects must not reference each other. They communicate only through messages.

[Theory]
[InlineData("Search", "Download")]
[InlineData("Download", "Search")]
public void Domains_DoNotReferenceEachOther(string from, string to)
{
    Types().That().ResideInNamespaceMatching($@"MyApp\.{from}(\..*)?")
        .Should().NotDependOnAny(Types().That().ResideInNamespaceMatching($@"MyApp\.{to}(\..*)?"))
        .Check(ArchitectureFixture.Architecture);
}

Naming conventions

Kind Rule
Actors *Manager (singleton), *Worker (entity), *Actor (other); derive from an actor base
Messages Commands VerbNoun, queries QueryNoun, events past tense
State <Actor>State, sealed class
[Fact]
public void ManagerClasses_AreActors()
{
    Classes().That().HaveNameEndingWith("Manager")
        .Should().BeAssignableTo(typeof(ActorBase))
        .Check(ArchitectureFixture.Architecture);
}

One class per file

ArchUnitNET inspects compiled types, so enforce this with a small file-based test or an analyzer rule (for example IDE0065 / a Roslyn analyzer) rather than the architecture model.

Allowed exceptions

Small wrapper records next to their state class and a command with its response records are deliberate groupings; whitelist them explicitly.

Do not over-constrain

Warning

Keep the rules at the same level of detail as the reference project. Every rule should encode a decision the team actually made and would defend. Rules that block legitimate refactors, or that restate what the compiler already enforces, create friction and get disabled.

  • Prefer a few rules on stable boundaries over many on incidental structure.
  • Failure messages should name the rule (.As("...") / Because("...")).
  • Never fix a violation by weakening the rule; fix the code or change the decision.