Unit Test your Architecture (and more) with ArchUnit
Yoan Thirion -
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.
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:
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:
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:
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:
Naming Convention
We may want to detect if we break any naming conventions like:
We can automate those checks like this:
Attributes usages
Often, we want to constrain some Annotation usages like:
Let's add those rules in our tests:
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<>:
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?