Explore the power of ArchUnit and learn how to bolster your software architecture testing.
Explore the power of ArchUnit
and learn how to bolster your software architecture testing. This guide will delve into the reasons why integrating ArchUnit can significantly improve your project's maintainability, reliability, and overall design. Discover the benefits of using ArchUnit
to enforce architectural rules, identify potential issues early on, and check your codebase adheres to your team practices continuously.
When working as a team, it is crucial to establish a shared vision for the software architecture. This involves deciding on a specific architecture style and designing systems accordingly.
However, as time progresses and more developers contribute to the project, there is a risk of deviating from the initially agreed-upon architecture style.
Goat Architecture
As a team, we are working on an ASP.NET system that adheres to Clean Architecture
principles to ensure scalability, maintainability, and testability.
Our architecture now consists of distinct layers with well-defined responsibilities and clear boundaries, organized around the core business logic.

This structure promotes a separation of concerns, facilitates easier testing and maintenance, and enables independence from external frameworks, databases, and user interfaces:
Domain (Entities)
: encapsulate enterprise wide business rules- Can be: Object with methods, set of data structures and functions
- Could be used by many different applications in the enterprise
Use Cases
: Capture application business rules- Structure should indicate what the application is, not how it does it
Controllers (Interface adapters)
: Set of adapters- Convert data from the format most convenient for the use cases and entities to the format most convenient for some external agency such as the Database or the Web
Repositories (Frameworks & drivers)
: Glue code that communicates to the next circle inwards- Frameworks and tools such as Database, Web Framework, ...
- This layer is where all the details go, keep these things on the outside where they can do little harm
We have a strong rule in our team:
It is basically the Dependency Inversion Principle
at the architecture level. If you want to know more about Clean Architecture you can read this post:

Ensure Dependency Inversion Principle
Here is the folder structure of our Service
:

We would like to automate the check of this principle that could be translated like this:
- Domain can only reference types in the Domain itself
- Use Cases can only reference types in Domain
- Controllers can reference types in Use Cases and Domain
- No constraint on Repositories
Let's implement those checks by using ArchUnitNET
.
ArchUnitNET
is a library inspired by the original Java-based ArchUnit, designed to create and test architectural rules within .NET applications. It provides a fluent API for developers to define and enforce architectural constraints, ensuring that the codebase adheres to desired patterns and principles.
We add the nuget
packages:
dotnet add package TngTech.ArchUnitNET.xUnit
Define our first Architecture Rule
We start by creating a new Unit Test
:
[Fact(DisplayName = "Lower layers can not depend on outer layers")]
public void CheckDependencyRule()
{
// Implement the Rule here
}
Then we use the fluent api to drive our implementation a.k.a Dot-Driven Development

After multiple refinements and adjustments, we arrive at a final version of our test that resembles the following structure:
[Fact(DisplayName = "Lower layers can not depend on outer layers")]
public void CheckDependencyRule()
{
// We declare our different layers
var domain = ArchRuleDefinition
.Types()
.That()
.ResideInNamespace("Domain", true);
var useCases = ArchRuleDefinition
.Types()
.That()
.ResideInNamespace("UseCases", true);
var controllers = ArchRuleDefinition
.Types()
.That()
.ResideInNamespace("Controllers", true);
var repositories = ArchRuleDefinition
.Types()
.That()
.ResideInNamespace("Repositories", true);
// We define an Architecture Rule
IArchRule rule = domain
.Should()
// Domain can only reference types in the Domain itself
.OnlyDependOn(domain)
.And(
// Use Cases can only reference types in Domain and in itself
useCases
.Should()
.NotDependOnAny(controllers).AndShould()
.NotDependOnAny(repositories)
)
.And(
// Controllers can reference types in Use Cases and Domain
controllers
.Should()
.NotDependOnAny(repositories)
);
// We define what is the Architecture to check the rule "against"
ArchUnitNET.Domain.Architecture architecture = new ArchLoader()
.LoadAssemblies(typeof(GoatController).Assembly)
.Build();
// We run the check
rule.Check(architecture);
}
Architecture Test
When we run it for the first time we get this feedback:

The library introspects our production code Assembly
and check if it ensures the defined rule. Here, it means that a GoatController
is using a Repositories
type:
public class GoatController(
FeedGoat feedGoat,
GoatRepository goatRepository)
{
...
}
The Port
interface (IGoatRepository
) should be used... Thanks ArchUnitNET
to detect it with an instant feedback loop (classic Unit Test).
What if the folders are split in projects?
Now, imagine we split the folders in projects:

We adapt our Architecture Test like this:
public class SplitArchitecture
{
private static readonly ArchUnitNET.Domain.Architecture Architecture =
new ArchLoader()
.LoadAssemblies(
// We load the different Assemblies to run the Check
typeof(Goat.Controllers.GoatController).Assembly,
typeof(Goat.UseCases.FeedGoat).Assembly,
typeof(Goat.Domain.Goat).Assembly,
typeof(Goat.Repositories.GoatRepository).Assembly
)
.Build();
private static GivenTypesConjunction TypesIn(string @namespace) =>
ArchRuleDefinition.Types().That().ResideInNamespace(@namespace, true);
private static GivenTypesConjunctionWithDescription Controllers() =>
TypesIn("Controllers").As("Interface Adapters");
private static GivenTypesConjunctionWithDescription UseCases() =>
TypesIn("UseCases").As("Application Business Rules");
private static GivenTypesConjunctionWithDescription Frameworks_Drivers() =>
TypesIn("Repositories").As("Framework & Drivers");
private static GivenTypesConjunctionWithDescription Domain() =>
TypesIn("Domain").As("Enterprise Business Rules");
[Fact(DisplayName = "Lower layers can not depend on outer layers")]
public void CheckRule() =>
Domain()
.Should()
.OnlyDependOn(Domain())
.And(
UseCases().Should()
.NotDependOnAny(Controllers()).AndShould()
.NotDependOnAny(Frameworks_Drivers())
)
.And(
Controllers().Should()
.NotDependOnAny(Frameworks_Drivers())
).Check(Architecture);
}
Architecture Test on split projects
And check that if a team member violates
accidentally our dependency rule it is detected:

If we run the test, we get this feedback:

We have our back covered đ€
Team Guidelines
ArchUnit
can help us define and check a lot of rules on our code. The examples below are not exhaustives and are there to demonstrate part of what you can do with it ("Sky is the limit").
Linguistic Anti-Patterns
Linguistic Anti-patterns (LAs) are inconsistencies between the signature (e.g., name, type), documentation (e.g., comments), and implementation of an entity (method or attribute). LAs are considered poor practices as when reading for example a method name, one may suppose a behaviour of the method that is different from the actual one.
More about LAs here:
Here are some easy tests to detect LAs:
public class LinguisticAntiPatterns
{
private static GivenMethodMembersThat Methods() => MethodMembers().That().AreNoConstructors().And();
[Fact]
// A method starting with Get should return something
public void NoGetMethodShouldReturnVoid() =>
Methods()
.HaveName("Get[A-Z].*", useRegularExpressions: true).Should()
.NotHaveReturnType(typeof(void))
.Check();
[Fact]
// A method starting with Is or Has should return a bool
public void IserAndHaserShouldReturnBooleans() =>
Methods()
.HaveName("Is[A-Z].*|Has[A-Z].*", useRegularExpressions: true)
.Should()
.HaveReturnType(typeof(bool))
.Check();
}
// Will detect those anti-patterns in Production code
public class ShittyMethods
{
public bool IsShitty() => false;
// Should return a bool
public void IsCorrect() { }
// Should return something
public void GetShitty() { }
public void HasShittyDef() { }
}
Detect Linguistic Anti-Patterns
Naming Convention
We may want to detect if we break any naming conventions like:
- A method must start by a capital letter
- An interface must start with I
- Every classes residing in the Services namespace must be suffixed with Service
- Every classes implementing the ICommandHandler interface must be suffixed with CommandHandler
...
Examples of naming convention
We can automate those checks like this:
public class NamingConvention
{
[Fact]
public void AllMethodsShouldStartWithACapitalLetter() =>
// We exclude methods generated at compile time like get_EqualityContract for example
MethodMembers()
.That().AreNoConstructors()
.And().DoNotHaveAnyAttributes("SpecialName|CompilerGenerated", true).Should()
.HaveName(@"^[A-Z]", true)
.Because("C# convention...")
.Check();
[Fact]
public void InterfacesShouldStartWithI() =>
Interfaces().Should()
.HaveName("^I[A-Z].*", useRegularExpressions: true)
.Because("C# convention...")
.Check();
[Fact]
public void ServicesShouldBeSuffixedByService() =>
Classes()
.That()
.ResideInNamespace("Services", true).Should()
.HaveNameEndingWith("Service")
.Check();
[Fact]
public void CommandHandlersShouldBeSuffixedByCommandHandler() =>
Classes()
.That()
.ImplementInterface(typeof(ICommandHandler<>)).Should()
.HaveNameEndingWith("CommandHandler")
.Check();
}
// Will detect those code in Production code
public class Goat
{
// Should start with a capital letter
void poorName()
{
}
}
// Should start with an I
public interface AGoat { }
public abstract record Command(Guid Id) { }
public interface ICommandHandler<in TCommand>
where TCommand : Command
{
int Handle(TCommand command);
}
public record Order(Guid Id) : Command(Id) { }
// Should end with CommandHandler
public class OrderService : ICommandHandler<Order>
{
public int Handle(Order command) => 42;
}
namespace Goat.Examples.Services
{
// Should end with Service
public class NotCompliantClass
{
}
}
Ensure naming convention
Attributes usages
Often, we want to constrain some Annotation
usages like:
- Controllers should reside only in the namespace "Controllers"
- The attribute ExcludeFromCoverage should not be used
Examples of attributes
Let's add those rules in our tests:
public class Annotations
{
private const string UseConfigFile = "You should use config file instead of ExcludeFromCodeCoverageAttribute";
private static readonly Type ExcludeFromCoverage = typeof(ExcludeFromCodeCoverageAttribute);
[Fact]
public void AnnotatedClassesShouldResideInAGivenNamespace() =>
Classes().That()
.HaveAnyAttributes(typeof(ApiControllerAttribute)).Should()
.ResideInNamespace("Controllers", true)
.Check();
[Fact]
public void CoverageAttributesShouldNotBeUsedOnClasses() =>
Classes()
.That().HaveAnyAttributes(ExcludeFromCoverage).Should()
.NotExist()
.Because(UseConfigFile)
.Check();
[Fact]
public void CoverageAttributesShouldNotBeUsedOnMethods() =>
MethodMembers()
.That().HaveAnyAttributes(ExcludeFromCoverage).Should()
.NotExist()
.Because(UseConfigFile)
.Check();
[Fact]
public void CoverageAttributesShouldNotBeUsedOnProperties() =>
PropertyMembers()
.That()
.HaveAnyAttributes(ExcludeFromCoverage).Should()
.NotExist()
.Because(UseConfigFile)
.Check();
}
// Will detect those code in Production code
// This attributes should not be used
[ExcludeFromCodeCoverage]
public class Feed
{
[ExcludeFromCodeCoverage] public string Name { get; set; }
[ExcludeFromCodeCoverage]
public void Again()
{
}
}
// Controllers should reside in "Controller" namespace
[ApiController]
public class GoatController
{
}
Ensure Annotations usages
Return Types
A last example of the power of this library is to check that the return type for a given types of objects is alway of the same type.
Imagine, we would like to standardize the response in our Api
using a dedicated type called ApiResponse<>
:
public class MethodReturnTypes
{
[Fact]
public void ControllersPublicMethodShouldOnlyReturnApiResponse() =>
MethodMembers().That()
.ArePublic().And()
.AreNoConstructors().And()
.AreDeclaredIn(Controllers()).Should()
.HaveReturnType(typeof(ApiResponse<>))
.Check();
private static GivenClassesConjunction Controllers() =>
Classes().That().HaveAnyAttributes(typeof(ApiControllerAttribute));
}
// Will detect those code in Production code
public record ApiError(string Code, string Message);\
public record ApiResponse<TData>(TData Data, ApiError[]? Errors = null);
[ApiController]
public class GoatControllerV2
{
public ApiResponse<int> Matching() => new ApiResponse<int>(42);
// This public method should return an ApiResponse
public void NotMatching()
{
}
}
Constrain return types
Conclusion
ArchUnit
is a powerful tool that can help us as developers to maintain and enforce architectural rules within our .NET applications. By integrating ArchUnit into our continuous integration pipeline, we can automatically detect violations, ensure consistency, and improve the overall software architecture.
As we explored its capabilities, consider the following food for reflection
:
- How can ArchUnit be tailored to fit your specific project requirements and architecture style?
- What architectural rules and constraints are most important for your project, and how can ArchUnit help enforce them?
- How can ArchUnit be integrated into your existing development workflow, and what benefits can it bring to your team's collaboration and code quality?
- What other tools or practices can you combine with ArchUnit to further enhance your software architecture testing and maintainability?
source code of this article
Join the conversation.