Tutorials

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

✍🏻 Author(s)

Leave a Reply