ArchUnit is a test library that allows us to verify that an application adheres to a given set of architectural rules or conventions of Java projects.
Why do we need to test our architecture?
The three main software quality goals to adhere to are maintainability, replace-ability, and extensibility. A great way to ensure these quality goals, is to ensure coding, structure and configuration consistency. Below are the reasons why consistency can change:
- Projects will often have developers joining or leaving with each having different coding styles.
- Projects tend to grow with lots of new features and codes.
- Adding new libraries.
- Using a lot of design patterns.
Why use ArchUnit?
ArchUnit library allows us to do the following checks:
- Package dependency checks.
- Class dependency checks.
- Class and package containment checks.
- Inheritance checks.
- Annotation checks.
- Layer checks.
- Cycle checks.
ArchUnit setup.
ArchUnit integrates nicely with the JUnit test framework.
<!-- Junit 4 -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit4</artifactId>
<version>0.15.0</version>
<scope>test</scope>
</dependency>
<!-- Junit 5 -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>0.15.0</version>
<scope>test</scope>
</dependency>
Examples:
Archunit offers a lot of different type of tests. Here we will cover some of the most common architectural features.
- Coding convention tests(Field injection, interface locations, logger instantiations) .
- Naming convention tests(Classes names with respect to their packages)
- Annotation convention tests(Annotations used in classes with respect to their packages)
- Layers convention test(classes dependencies)
Source code
All codes are available at: https://github.com/hramdin28/spring-archunit-test
Project structure

1. Coding conventions test
@ArchTag("CodingConventionTest")
@AnalyzeClasses(packages = "com.hanish", importOptions = {
ImportOption.DoNotIncludeTests.class, // Do not scan Test classes
ImportOption.DoNotIncludeJars.class, // Do not scan Jars
ImportOption.DoNotIncludeArchives.class}) // Do not use Archives
public class CodingConventionTest {
/**
* Check field injection is not used in classes
*/
@ArchTest
private final ArchRule no_field_injection = NO_CLASSES_SHOULD_USE_FIELD_INJECTION;
/**
* Check interfaces are not available in packages ..impl..
*/
@ArchTest
static final ArchRule interfaces_must_not_be_placed_in_implementation_packages =
noClasses()
.that().resideInAPackage("..impl..")
.should().beInterfaces();
/**
* Check loggers are private, static, final
*/
@ArchTest
private final ArchRule loggers_should_be_private_static_final =
fields()
.that()
.haveRawType(Logger.class)
.should().bePrivate()
.andShould().beStatic()
.andShould().beFinal();
}
2. Naming Convention Test
@ArchTag("NamingConventionTest")
@AnalyzeClasses(packages = "com.hanish", importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class,
ImportOption.DoNotIncludeArchives.class})
class NamingConventionTest {
/**
* Check classes/interfaces names in packages ..repository ends with Repository
*/
@ArchTest
static ArchRule repositories_should_end_with_Repository =
classes()
.that().resideInAPackage("..repository")
.should().haveSimpleNameEndingWith("Repository");
/**
* Check classes/interfaces names in packages ..service ends with Service
*/
@ArchTest
static ArchRule services_should_end_with_Service =
classes()
.that().areInterfaces()
.and().resideInAPackage("..service")
.should().haveSimpleNameEndingWith("Service");
/**
* Check classes/interfaces names in packages ..service.impl ends with ServiceImpl
*/
@ArchTest
static ArchRule services_impl_should_end_with_ServiceImpl =
classes()
.that().resideInAPackage("..service.impl")
.should().haveSimpleNameEndingWith("ServiceImpl");
/**
* Check classes names in packages ..controller ends with Controller
*/
@ArchTest
static ArchRule controllers_should_end_with_Controller =
classes()
.that().resideInAPackage("..controller")
.should().haveSimpleNameEndingWith("Controller");
}
3. Annotations convention test
@ArchTag("StereotypeConventionTest")
@AnalyzeClasses(packages = "com.hanish", importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class,
ImportOption.DoNotIncludeArchives.class})
public class AnnotationConventionTest {
/**
* Check classes/Interfaces in package ..repository has annotation @Repository
*/
@ArchTest
static ArchRule repository_classes_reside_in_repository_package =
classes()
.that().areAnnotatedWith(Repository.class)
.should().resideInAPackage("..repository");
/**
* Check classes in package ..service.impl has annotation @Service
*/
@ArchTest
static ArchRule service_classes_reside_in_service_impl_package =
classes()
.that().areAnnotatedWith(Service.class)
.should().resideInAPackage("..service.impl");
/**
* Check classes in package ..controller has annotation @RestController
*/
@ArchTest
static ArchRule controller_classes_reside_in_controller_package =
classes()
.that().areAnnotatedWith(RestController.class)
.should().resideInAPackage("..controller");
/**
* Check classes in package ..model has annotation @Entity
*/
@ArchTest
static ArchRule entity_classes_reside_in_model_package =
classes()
.that().areAnnotatedWith(Entity.class)
.should().resideInAPackage("..model");
}
4. Layers convention test
@ArchTag("LayerConventionTest")
@AnalyzeClasses(packages = "com.hanish", importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class,
ImportOption.DoNotIncludeArchives.class})
public class LayerConventionTest {
/**
* Layers testing:
* Check Controllers not accessed by other classes
* Check Services are only accessed by Controllers and Main application class
* Check Repositories are only accessed by services
*/
@ArchTest
static final ArchRule layer_dependencies_are_respected = layeredArchitecture()
.layer("Controllers").definedBy("com.hanish.controller..")
.layer("Services").definedBy("com.hanish.service..")
.layer("Repositories").definedBy("com.hanish.repository..")
.layer("Application").definedBy("com.hanish..")
.whereLayer("Controllers").mayNotBeAccessedByAnyLayer()
.whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers", "Application")
.whereLayer("Repositories").mayOnlyBeAccessedByLayers("Services");
}