ArchUnit allows developers to enforce architecture rules such as naming conventions, class access to other classes, and the prevention of cycles. The library was originally created in 2017 by Peter Gafert and version 1.0.0 was released in October.
ArchUnit works with all Java test frameworks and offers specific dependencies for JUnit. The following dependency should be used for JUnit 5:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
Now the ClassFileImporter
can be used to import Java bytecode into Java code. For example, to import all classes in the org.example package:
JavaClasses javaClasses = new ClassFileImporter().importPackages("org.example");
Now the ArchRule
class may be used to define architectural rules for the imported Java classes in a Domain Specific Language (DSL). There are various types of checks available, the first one is for package dependencies. The check specifies that no classes inside repository packages should use classes inside controller packages:
ArchRule rule = noClasses()
.that().resideInAPackage("..repository..")
.should().dependOnClassesThat().resideInAPackage("..controller..");
Two classes are used to verify the rules, a CourseController
class inside a controller package and a CourseRepository
class inside a repository package:
public class CourseController {
private CourseRepository courseRepository;
}
public class CourseRepository {
CourseController courseController;
}
This is not allowed by the ArchRule
defined before, which can be tested automatically with JUnit:
AssertionError assertionError =
Assertions.assertThrows(AssertionError.class, () -> {
rule.check(javaClasses);
});
String expectedMessage = """
Architecture Violation [Priority: MEDIUM] -
Rule 'no classes that reside in a package
'..repository..' should depend on classes that reside in a package
'..controller..'' was violated (1 times):
Field <org.example.repository.CourseRepository.courseController> has type
<org.example.controller.CourseController> in (CourseRepository.java:0)""";
assertEquals(expectedMessage, assertionError.getMessage());
The CourseController
and CourseRepository
depend on each other, which often is a design flaw. The cycle check detects cycles between classes and packages:
ArchRule rule = slices()
.matching("org.example.(*)..")
.should().beFreeOfCycles();
AssertionError assertionError =
Assertions.assertThrows(AssertionError.class, () -> {
rule.check(javaClasses);
});
String expectedMessage = """
Architecture Violation [Priority: MEDIUM] - Rule 'slices matching
'org.example.(*)..' should be free of cycles' was violated (1 times):
Cycle detected: Slice controller ->\s
Slice repository ->\s
Slice controller
1. Dependencies of Slice controller
- Field <org.example.controller.CourseController.courseRepository> has type
<org.example.repository.CourseRepository> in (CourseController.java:0)
2. Dependencies of Slice repository
- Field <org.example.repository.CourseRepository.courseController> has type
<org.example.controller.CourseController> in (CourseRepository.java:0)""";
assertEquals(expectedMessage, assertionError.getMessage());
Class and Package containment checks allow the verification of naming and location conventions. For example, to verify that no interfaces are placed inside implementation packages:
noClasses()
.that().resideInAPackage("..implementation..")
.should().beInterfaces().check(classes);
Or to verify that all interfaces have a name containing "Interface":
noClasses()
.that().areInterfaces()
.should().haveSimpleNameContaining("Interface").check(classes);
These containment checks may be combined with an annotation check. For example, to verify that all classes in the controller package with a RestController
annotation have a name ending with Controller:
classes()
.that().resideInAPackage("..controller..")
.and().areAnnotatedWith(RestController.class)
.should().haveSimpleNameEndingWith("Controller");
Inheritance checks allow, for example, to verify that all classes implementing the Repository
interface have a name ending with Repository:
classes().that().implement(Repository.class)
.should().haveSimpleNameEndingWith("Repository")
With the layer checks, it's possible to define the architecture layers of an application and then define the rules between the layers:
Architectures.LayeredArchitecture rule = layeredArchitecture()
.consideringAllDependencies()
// Define layers
.layer("Controller").definedBy("..controller..")
.layer("Repository").definedBy("..Repository..")
// Add constraints
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Controller");
AssertionError assertionError =
Assertions.assertThrows(AssertionError.class, () -> {
rule.check(javaClasses);
});
String expectedMessage = """
Architecture Violation [Priority: MEDIUM] - Rule 'Layered architecture
considering all dependencies, consisting of
layer 'Controller' ('..controller..')
layer 'Repository' ('..Repository..')
where layer 'Controller' may not be accessed by any layer
where layer 'Repository' may only be accessed by layers ['Controller']'
was violated (2 times):
Field <org.example.repository.CourseRepository.courseController> has type
<org.example.controller.CourseController> in (CourseRepository.java:0)
Layer 'Repository' is empty""";
assertEquals(expectedMessage, assertionError.getMessage());
More information can be found in the extensive user guide and official examples from ArchUnit are available on GitHub.