Java Architecture Unit Test (ArchUnit)
ArchUnit is a library with which the architecture of our Java code can be verified. With this library, we can verify different aspects such as: dependencies between packages and classes, label, nomenclature, etc.
To use this library, we have two options:
- Make use of the Initializr to create our project: which by default adds the dependency in the pom.xml. This is the fastest, perfect option.
- In case we have a service already created: and therefore we cannot use the Initialzr, we will have to add the dependency to the pom file manually.
Assuming our case is the second one, we will have to generate the dependency by adding the following code in our project’s pom.xml file:
<!-- https://mvnrepository.com/artifact/com.tngtech.archunit/archunit-junit5-api -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>0.14.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
On the other hand, we must create inside a file in the «resources» folder of the tests. This file will be called «archunit.properties», and within it we will insert the following line of code:
resolveMissingDependenciesFromClassPath=false
Use examples
If we are using the Initializr, four tests are included by default. These verify that the implementation of controllers, services and DTOs follow the architecture standard. Let’s have a look at some examples:
Example controller and service log check
public class AnnotationsTest {
private JavaClasses javaClasses;
@BeforeEach
public void init() {
this.javaClasses = new ClassFileImporter().importPackages("com.minsait.onesait.pruebaarchunit");
}
@Test
public void givenControllers_thenClassesAreAnnotatedWithRestController() {
ArchRule controllerRule = classes().that().resideInAPackage("..controller..").should()
.beAnnotatedWith(RestController.class);
controllerRule.check(this.javaClasses);
}
@Test
public void givenServices_thenClassesAreAnnotatedWithService() {
ArchRule serviceRule = classes().that().resideInAPackage("..services.impl").should()
.beAnnotatedWith(Service.class);
serviceRule.check(this.javaClasses);
}
}
Example of nomenclature, field access and interface checking
public class ClassesTest {
private JavaClasses javaClasses;
@BeforeEach
public void init() {
this.javaClasses = new ClassFileImporter().importPackages("com.minsait.onesait.pruebaarchunit");
}
@Test
public void givenClasses_thenAreContainedInCorrectPackages() {
ArchRule rule1 = classes()
.that().haveSimpleNameEndingWith("Controller")
.should().resideInAPackage("..controllers");
ArchRule rule2 = classes()
.that().haveSimpleNameEndingWith("Service")
.should().resideInAPackage("..services");
ArchRule rule3 = classes()
.that().resideInAPackage("..model")
.and().areNotNestedClasses()
.should().haveSimpleNameEndingWith("DTO");
rule1.check(this.javaClasses);
rule2.check(this.javaClasses);
rule3.check(this.javaClasses);
}
@Test
public void givenModelDTOAndEntity_thenFieldsArePrivate() {
this.javaClasses = new ClassFileImporter().importPackages("com.minsait.onesait.pruebaarchunit.model");
ArchRule dtoRule = fields()
.should().bePrivate();
dtoRule.check(this.javaClasses);
}
@Test
public void givenService_thenMustBeInterfaces() {
ArchRule rule1 = classes()
.that().resideInAPackage("..services")
.should().beInterfaces();
rule1.check(this.javaClasses);
}
}
Checking for non-insertion of interfaces in deployment packages
@AnalyzeClasses(packages = "com.minsait.onesait.pruebaarchunit")
public class CodingRulesTest {
@ArchTest
private final ArchRule no_generic_exceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;
@ArchTest
private final ArchRule interfaces_must_not_be_placed_in_implementation_packages = noClasses().that()
.resideInAPackage("..impl..").should().beInterfaces();
}
Check dependency between classes and access between them
public class LayerTest {
private JavaClasses javaClasses;
@BeforeEach
public void init() {
this.javaClasses = new ClassFileImporter().importPackages("com.minsait.onesait.pruebaarchunit");
}
@Test
public void givenControllerLayer_thenDependsOnServiceLayer() {
ArchRule controllerRule = classes()
.that()
.resideInAPackage("..controller..")
.should().dependOnClassesThat()
.resideInAPackage("..service..");
controllerRule.check(this.javaClasses);
}
@Test
public void givenALayerArchitecture_thenNoLayerViolationShouldExist() {
LayeredArchitecture architecture = layeredArchitecture()
.layer("controller").definedBy("..controllers..")
.layer("service").definedBy("..services.impl")
.whereLayer("controller").mayNotBeAccessedByAnyLayer()
.whereLayer("service").mayOnlyBeAccessedByLayers("controller");
architecture.check(this.javaClasses);
}
}
References: for more information, you can check the official ArchUnit documentation.
Header: photo by Shapelined at Unsplash