Spring Boot - Unit Test your project architecture with ArchUnit
Join the DZone community and get the full member experience.
Join For FreeWhen building software, it's common for development teams to define a set of guidelines and code conventions that are considered best practices.
These are practices that are generally documented and communicated to the entire development team that has accepted them. However, during development, developers can violate these guidelines which are discovered during code reviews or with code quality checking tools.
An important aspect is therefore to automate these directives as much as possible over the entire project architecture in order to optimize the reviews.
We can impose these guidelines as verifiable JUnit tests using ArchUnit. It guarantees that a software version will be discontinued if an architectural violation is introduced.
ArchUnit is a free, simple and extensible library for checking the architecture of your Java code using any plain Java unit test framework. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more. It does so by analyzing given Java bytecode, importing all classes into a Java code structure.
ArchUnit lets you implement rules for the static properties of application architecture in the form of executable tests such as the following:
- Package dependency checks
- Class dependency checks
- Class and package containment checks
- Inheritance checks
- Annotation checks
- Layer checks
- Cycle checks
Getting Started
ArchUnit’s JUnit 5 support, simply add the following dependency from Maven Central:
pom.xml
xxxxxxxxxx
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>0.14.1</version>
<scope>test</scope>
</dependency>
build.gradle
xxxxxxxxxx
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:0.14.1'
}
Package Dependency Checks
xxxxxxxxxx
class ArchunitApplicationTests {
private JavaClasses importedClasses;
public void setup() {
importedClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.springboot.testing.archunit");
}
void servicesAndRepositoriesShouldNotDependOnWebLayer() {
noClasses()
.that().resideInAnyPackage("com.springboot.testing.archunit.service..")
.or().resideInAnyPackage("com.springboot.testing.archunit.repository..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("com.springboot.testing.archunit.controller..")
.because("Services and repositories should not depend on web layer")
.check(importedClasses);
}
}
Services and Repositories should not talk to Web layer.
Class Dependency Checks
xxxxxxxxxx
class ArchunitApplicationTests {
private JavaClasses importedClasses;
public void setup() {
importedClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.springboot.testing.archunit");
}
void serviceClassesShouldOnlyBeAccessedByController() {
classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..service..", "..controller..")
.check(importedClasses);
}
}
ArchUnit offers an abstract DSL-like fluent API, which can in turn be evaluated against imported classes. Services should only be accessed by Controllers .
The two dots represent any number of packages (compare AspectJ Pointcuts).
Naming convention
xxxxxxxxxx
class ArchunitApplicationTests {
private JavaClasses importedClasses;
public void setup() {
importedClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.springboot.testing.archunit");
}
void serviceClassesShouldBeNamedXServiceOrXComponentOrXServiceImpl() {
classes()
.that().resideInAPackage("..service..")
.should().haveSimpleNameEndingWith("Service")
.orShould().haveSimpleNameEndingWith("ServiceImpl")
.orShould().haveSimpleNameEndingWith("Component")
.check(importedClasses);
}
void repositoryClassesShouldBeNamedXRepository() {
classes()
.that().resideInAPackage("..repository..")
.should().haveSimpleNameEndingWith("Repository")
.check(importedClasses);
}
void controllerClassesShouldBeNamedXController() {
classes()
.that().resideInAPackage("..controller..")
.should().haveSimpleNameEndingWith("Controller")
.check(importedClasses);
}
}
A common rule is the naming convention. for example all service class names must end with Service, Component etc.
Annotation Checks
xxxxxxxxxx
class ArchunitApplicationTests {
private JavaClasses importedClasses;
public void setup() {
importedClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.springboot.testing.archunit");
}
void fieldInjectionNotUseAutowiredAnnotation() {
noFields()
.should().beAnnotatedWith(Autowired.class)
.check(importedClasses);
}
void repositoryClassesShouldHaveSpringRepositoryAnnotation() {
classes()
.that().resideInAPackage("..repository..")
.should().beAnnotatedWith(Repository.class)
.check(importedClasses);
}
void serviceClassesShouldHaveSpringServiceAnnotation() {
classes()
.that().resideInAPackage("..service..")
.should().beAnnotatedWith(Service.class)
.check(importedClasses);
}
}
The ArchUnit Lang API can define rules for members of Java classes. This may be relevant, for example, if methods in a certain context need to be annotated with a specific annotation, or if return types implement a certain interface.
Layer Checks
xxxxxxxxxx
class ArchunitApplicationTests {
private JavaClasses importedClasses;
public void setup() {
importedClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.springboot.testing.archunit");
}
void layeredArchitectureShouldBeRespected() {
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service")
.check(importedClasses);
}
}
In Spring Boot app, Service layer depends on Repository layer, Controller layer depend on Service layer.
ArchUnit offers a set of features to assert that your layered architecture is respected. These tests provide automated assurances that access and use is maintained within your defined limits. it is therefore possible to write custom rules. In this article, we have just described a few of the rules. The official ArchUnit guide presents the different possibilities.
The complete source code can be found in my GitHub repository.
Opinions expressed by DZone contributors are their own.
Comments