Unit Test your Architecture (and more) with ArchUnit

Yoan Thirion
Yoan Thirion
Unit Test your Architecture (and more) with ArchUnit
Unit Test your Architecture (and more) with ArchUnit
Table of Contents
Table of Contents

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.

What if we could address the issue of maintaining architectural consistency by implementing tests that enforce architectural rules, detect violations, and provide valuable feedback to developers?

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.

Clean Architecture

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:

⚠️
Outer layers can depend on lower layers, but no code in the lower layer can depend directly on any code in the outer layer.

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:

Clean Architecture | Xtrem T.D.D
Clean architecture is a software design approach that separates the elements of a design into ring levels.

Ensure Dependency Inversion Principle

Here is the folder structure of our Service :

Project structure

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.

ArchUnitNET
None

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

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:

Failed architecture test

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:

From folders to 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:

Accidental reference to the Repositories

If we run the test, we get this feedback:

Failed Architecture Tests for accidental reference

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:

LAs – Venera Arnaoudova

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:

  1. How can ArchUnit be tailored to fit your specific project requirements and architecture style?
  2. What architectural rules and constraints are most important for your project, and how can ArchUnit help enforce them?
  3. How can ArchUnit be integrated into your existing development workflow, and what benefits can it bring to your team's collaboration and code quality?
  4. What other tools or practices can you combine with ArchUnit to further enhance your software architecture testing and maintainability?
goat/archunit at main · ythirion/goat
Contribute to ythirion/goat development by creating an account on GitHub.

source code of this article



Join the conversation.

Great! Check your inbox and click the link
Great! Next, complete checkout for full access to Goat Review
Welcome back! You've successfully signed in
You've successfully subscribed to Goat Review
Success! Your account is fully activated, you now have access to all content
Success! Your billing info has been updated
Your billing was not updated