Spring Boot 3 — Unit Testing project Architecture with ArchUnit

In this post, we’ll explore the power of ArchUnit in the Spring Boot application.

· Prerequisites
· Overview
∘ What is ArchUnit?
∘ Why use ArchUnit?
· Let’s get to the code
∘ Project Architecture
∘ Add ArchUnit Dependency
∘ ArchUnit Rules Examples
∘ Run the Tests
· Conclusion
· References


Prerequisites

This is the list of all the prerequisites:

  • Spring Boot 3+
  • Maven 3.6.3
  • Java 21
  • Basic knowledge with Junit 5

Overview

When building software, it’s common for development teams to define a set of guidelines and code conventions considered best practices.

These practices are generally documented and communicated to the entire development team that has accepted them. However, during development, developers can inadvertently violate these guidelines, as discovered during code reviews or through code quality checking tools.

Therefore, to optimize the reviews, it is important to automate these directives as much as possible across the entire project architecture.

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.

What is ArchUnit?

ArchUnit is a Java library designed to check the architecture of your code. It can check for cyclic dependencies, layers and slices, and dependencies between packages and classes, among other things. It analyzes given Java bytecode and imports all classes into a Java code structure. ArchUnit’s main focus is automatically testing architecture and coding rules, using any plain Java unit testing framework.

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

Why use ArchUnit?

  • It’s open-source and free to use.
  • It integrates testing frameworks JUnit 4 and 5 and is easy to use.
  • ArchUnit helps prevent the accumulation of technical debt.
  • ArchUnit tests can be run as part of your build process, allowing you to catch architectural violations early in the development cycle.
  • It improves code maintainability
  • ArchUnit allows you to define rules to check for cyclic dependencies, preventing this kind of scenario from emerging in the first place.
  • It works in harmony with other tools, such as JUnit, Mockito, and static analysis tools (e.g., Checkstyle, PMD, and SonarQube).

Let’s get to the code

Project Architecture

The ArchUnit test rules for this story will be based on the Spring Boot sample architecture below:

Add ArchUnit Dependency

To use ArchUnit in combination with JUnit 5, include the following dependency from Maven Central:

pom.xml

<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>

build.gradle

dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
}

ArchUnit Rules Examples

To use the JUnit support, declare the classes to import via @AnalyzeClasses and add the respective rules as fields:

@AnalyzeClasses(packagesOf = ArchunitApplication.class, importOptions = ImportOption.DoNotIncludeTests.class)
class ArchUnitApplicationTests {

@ArchTest
static final ArchRule repositoriesMustResideInRepositoryPackage =
classes().that().haveNameMatching(".*Repository").should().resideInAPackage("..repository..")
.as("Repositories should reside in a package '..repository..'");
}

The JUnit test support will automatically import (or reuse) the specified classes and evaluate any rule annotated with @ArchTest against those classes.

Layer Checks

    @ArchTest
static final ArchRule layeredArchitecture = Architectures.layeredArchitecture()
.consideringOnlyDependenciesInLayers()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")

.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");

In the Spring Boot app, the Service layer depends on the Repository layer, Controller layer depends on the Service layer.

Annotation Checks

    @ArchTest
static final ArchRule fieldInjectionNotUseAutowiredAnnotation = noFields()
.should().beAnnotatedWith(Autowired.class);


@ArchTest
static final ArchRule repositoryClassesShouldHaveSpringRepositoryAnnotation = classes()
.that().resideInAPackage("..repository..")
.should().beAnnotatedWith(Repository.class);


@ArchTest
static final ArchRule serviceClassesShouldHaveSpringServiceAnnotation = classes()
.that().resideInAPackage("..service..")
.and().haveSimpleNameEndingWith("ServiceImpl")
.should().beAnnotatedWith(Service.class);

@ArchTest
static final ArchRule controllerClassesAnnotations = classes()
.that().resideInAPackage("..controller..")
.should().beAnnotatedWith(RestController.class).orShould().beAnnotatedWith(RequestMapping.class);

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.

Naming convention

    @ArchTest
static final ArchRule serviceClassesNaming = classes()
.that().resideInAPackage("..service..")
.should().haveSimpleNameEndingWith("Service")
.orShould().haveSimpleNameEndingWith("ServiceImpl")
.orShould().haveSimpleNameEndingWith("Component");

@ArchTest
static final ArchRule repositoryClassesNaming = classes()
.that().resideInAPackage("..repository..")
.should().haveSimpleNameEndingWith("Repository");

@ArchTest
static final ArchRule controllerClassesNaming = classes()
.that().resideInAPackage("..controller..")
.should().haveSimpleNameEndingWith("Controller");

A common rule is the naming convention. for example, all service class names must end with Service, Component, etc.

Class Dependency Checks

    @ArchTest
static final ArchRule servicesAndRepositoriesShouldNotDependOnWebLayer = noClasses()
.that().resideInAnyPackage("..service..")
.or().resideInAnyPackage("..repository..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("..controller..")
.because("Services and repositories should not depend on web layer");

@ArchTest
static final ArchRule repositoriesMustResideInRepositoryPackage =
classes().that().haveNameMatching(".*Repository").should().resideInAPackage("..repository..")
.as("Repositories should reside in a package '..repository..'");

@ArchTest
static final ArchRule domainClassesShouldBePublic =
classes()
.that().resideInAPackage("..entity..")
.should()
.bePublic();

@ArchTest
static final ArchRule domainClassesShouldBeSerializable =
classes()
.that().resideInAPackage("..entity..")
.should()
.beAssignableTo(Serializable.class);

Run the Tests

Once you have written your architecture tests, run them as regular JUnit tests. If your project violates any of the defined rules, ArchUnit will throw an exception, and your test will fail, helping you identify architectural issues early.

Conclusion

Well done !!.

ArchUnit offers features to assert that your layered architecture is respected. These tests provide automated assurances that access and use are maintained within your defined limits. It is therefore possible to write custom rules. In this story, we have just described a few of the rules. The official ArchUnit guide presents the different possibilities.

The complete source code is available on GitHub.

This post is an improved version of the original version that I published on https://dzone.com

Support me through GitHub Sponsors.

Thank you for reading!! See you in the next post.

References

👉 Link to Medium blog

Related Posts