ArchUnit
In a given project, a well-structured architecture is one of the fundamental pillars for sustainable development over time. Today, in the latest Inspiring Technology our Hunters share with us all you need to achieve it: ArchUnit.
As well as defining an architecture, you have to ensure that it is applied in the project. Initially, implementation is usually done according to the proposed architecture, but as time goes by, complexity increases in the project’s functionalities, new developers come in and experienced developers leave, often causing the project to deviate from its initial architecture. Checking compliance with the architecture for every change and development within the project is a manual task carried out by people with extensive knowledge. Often these checks are not as thorough as they ought to be due to lack of time, taking up time that could be devoted to other more specific reviews.
ArchUnit is a library that allows you to define automated tests to ensure that a project respects the architecture it is based on. These tests are easy to define and quickly executed as JUnit tests. It also simplifies code reviews by allowing the reviewer to focus on more specific aspects. It helps less experienced developers to understand the project architecture by being able to check whether the changes made are correct at any time. To run it, architectural rules are defined with the ArchUnit API. During testing, the generated bytecode is analysed and checked for compliance with the rules.
Project integration
ArchUnit is integrated into test execution, as it has a native implementation with JUnit allowing it to be adopted quickly and easily in most projects. It has explicit JUnit 4 and JUnit 5 support, although it can be used with any test automation framework running on Java.
To start with, the library needs to be added as a dependency of the project.
JUnit dependency with Maven project manager.
Once the library is included, automated tests are defined as JUnit tests as follows:
- The base package containing the classes to be verified is indicated with @AnalyzeClasses.
- Test methods are indicated with @ArchTest instead of @Test.
- @ArchTest test methods receive a JavaClasses object as a parameter, which will contain references to the classes to be tested.
ArchUnit structure test with JUnit.
Common architecture checks
ArchUnit offers a set of predefined rules to make it easy to check the correct use of n-tier, onion or hexagonal architectures. To illustrate these rules, an example project is taken with an n-tier architecture, where the Presentation layer accesses the Service layer which, in turn, accesses the Persistence layer. In this case, for example, access to the Presentation layer through the Service or Persistence layer is restricted.
Example of relationship between layers.
To ensure that these constraints are met, the following test is defined:
Example of relationship between layers.
This table names each of the layers and shows in which package they reside. The constraints to be met between layers are then defined:
- No layer can access the Presentation layer.
- The Service layer can only be accessed through the Presentation layer.
- The Persistence layer can only be accessed through the Service layer.
This way, if any of these constraints are not met, such as where a service layer method depends on a presentation layer method, this automated test will fail.
ArchUnit API
ArchUnit provides a simple API that allows a clear definition of architectural rules to define tests.
To define a rule, you select to which unit it applies, allowing you to choose between classes, methods, constructors, members, fields and the negated versions of each (e.g. noClasses). You then enter the filters you want to set and finally the checks.
The following example checks that no classes that do not reside in the services, persistence or mappers package depend on classes that reside in the entities package.
Example set up.
Service structure use case
ArchUnit provides tools to define rules tailored to project specifications. In the example shown below, the following rules are implemented for the services of a Spring Boot project:
- Service interfaces are defined in the services package and service implementations are defined within the services.impl package.
- The client code will always depend on the service interface, never on the service itself.
- Only service classes reside within the services.impl package.
- No other application package contains more service classes.
Example test for package only composed of interfaces.
To ensure that the restrictions are met, a rule is first defined that checks that only interfaces are defined in the services package.
Example test for package only composed of classes.
A rule is defined to ensure that at no point you directly depend on classes residing in service.impl. In this way, it will depend on the service interfaces and the framework will be in charge of injecting the corresponding class.
Example test for dependence on service classes.
A rule is defined to ensure that the service.impl package is composed exclusively of services, i.e. classes annotated with _@Service_.
Example test for package only composed of classes.
Finally, a rule is defined to ensure that no class annotated with _@Service_ exists outside the service.impl package.
Example test for services out of services.impl package.
Descriptive error messages
In the case of a rule violation, ArchUnit takes care of generating readable error text to help understand the problem. Also, text can be associated with a rule to explain the motivation behind it.
In the following example test, a rule is defined that checks if there is a class annotated with @RestController outside the controllers package.
Example test for services out of services.impl package.
The following is an error message generated when this rule fails. For this test, the @RestController annotation has been added to the ClientDTO class which is outside the controllers package.
Example error message.
As you can see, ArchUnit creates a text explaining the rule and then displays the text with the explanation. At the end of the error message, you are explicitly shown which problem occurred and where.
ArchUnit in C# projects
ArchUnit can run on any project that uses Java virtual machine-based languages, such as Kotlin, Scala, etc.
For use in projects using the C# language, there is a fork called ArchUnitNet.
Advantages
- Ensures compliance with the architecture defined for the project in an automated way.
- Helps people new to the project to understand the architecture.
- Simplifies code reviews.
- Compatible with Java-based languages.
- Rapid execution of tests.
- Simple definition of tests.
Disadvantages
- ArchUnit analyses bytecode, complicating tests on code generators such as Lombok or MapStruct.
Conclusions
The use of ArchUnit can have a major impact within a project, ensuring compliance with the proposed architecture, making it easier for experienced developers to review code and for new developers to join the project. Also, it can be easily implemented as integrated unit tests within the project.
Watch this video to see how it works:
A continuación, podemos ver un vídeo de su funcionamiento:
Want to know more about Hunters?
A Hunter rises to the challenge of trying out new solutions, delivering results that make a difference. Join the Hunters programme and become part of a diverse group that generates and transfers knowledge.
Anticipate the digital solutions that will help us grow. Find out more about Hunters on our website.