ArchUnit, Unit Testing The Architecture
ArchUnit will help you whenever the compiler will not make it, especially in enforcing a package structure or architecture.
Join the DZone community and get the full member experience.
Join For FreeEnforcing a specific package structure or architecture is very important. Especially in Java, where some things must be public to work correctly or actually be available outside their package. ArchUnit is an open-source library that will help you whenever the compiler is not enough.
All of the code examples from this article are available in my GitHub repo.
What Is ArchUnit?
ArchUnit is an open-source library for writing and enforcing architecture rules within your project. There is no use of facades and encapsulations when you can just reach out and take what you want. It is even more significant when you are trying to follow ports and adapters or other approaches that impose very strict restrictions on which classes should use others and in what way.
That is the moment where ArchUnit comes into play. It can easily check the dependencies between packages and classes, which class is using/importing the other. With this “simple” feature, we can easily set up a set of rules that will put restrictions on how our classes can interact with one another. Later, we can easily add these rules to our test suite and, by extension, unit test our architecture.
For all designs and purposes, the rules are normal tests and can be easily run by any unit test library/framework. Besides “simple” packages and classes dependencies check mentioned above, it can also check dependencies between layers and slices, check for cyclic dependencies, and more.
We can create the following rules with the help of ArchUnit:
- “Classes in package X should only depend on classes in package Y.”
- “Classes in the service layer should not access controller layer classes.”
- “No cyclic dependencies should exist among these packages.”
- Prevent a field and setter-based injection
- Ensure
@Transactionalannotation is used only in the service layer - Enforce
@Repositoryand@Serviceannotation usage in specific packages
Without going into much detail, ArchUnit works by reading and analyzing bytecode, not the source code itself. Thus, our rules are not applied to source code per se but rather to output bytecode.
ArchUnit-Junit
ArchUnit-Junit artifact is part of the wider ArchUnit framework. It makes the tests more descriptive and smaller by removing a lot of JUnit-related boilerplate code. The most import part of this package is ArchTest annotation. By using it, we can write tests as methods, not JUnit tests. The artifact will take care of actually converting the method into a proper JUnit test.
ArchUnit-Junit
@ArchTest
static final ArchRule classesInXShouldOnlyDependOnClassesInY =
ArchRuleDefinition
.classes()
.that().resideInAPackage("..x..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage(
"..y..",
"java.."
);
JUnit
@Test
void testClassesInXShouldOnlyDependOnClassesInY() {
ArchRule rule = classes()
.that().resideInAPackage("..x..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage(
"..y..",
"java.."
);
rule.check(IMPORTED_CLASSES);
}
The difference seems no big deal, however, I can see potential benefits if you have a lot of tests. Personally, I prefer the classic JUnit way.
All the tests written here follow a JUnit way without the archunit-junit5-engine. Nevertheless, you can find the examples written with junit-archunit lib in the repo.
ArchUnit Examples
Let’s start with the implementation of all the rules from above. Then I will move on to presenting rules that will ensure your ports and adapters setup remains unchanged.
Classes in package X should only depend on classes in package Y.
@Test
void testClassesInXShouldOnlyDependOnClassesInY() {
// Given: Define a rule that restricts classes in package '..x..'
// to depend only on classes in '..y..' or standard Java packages.
ArchRule rule = classes()
.that().resideInAPackage("..x..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage(
"..y..", // Allow dependency on package '..y..'
"java.." // Allow dependency on Java standard library
);
// Then
rule.check(IMPORTED_CLASSES);
}
Classes in the service layer should not access controller layer classes.
@Test
void testServiceLayerShouldNotAccessControllers() {
// Given: Define a rule that prevents the service layer
// from depending on classes in the controller layer.
ArchRule rule = noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat()
.resideInAPackage("..controller..");
// Then
rule.check(IMPORTED_CLASSES);
}
No cyclic dependencies should exist among packages.
@Test
void testNoCyclicDependencies() {
// Given: Define a rule to ensure there are no cyclic dependencies
// between modules grouped by their first-level sub-packages under 'org.ps'.
ArchRule rule = SlicesRuleDefinition.slices()
.matching("org.ps.(*)..") // Define slices by sub-packages under 'org.ps'
.should()
.beFreeOfCycles(); // Ensure there's no cyclic dependency between them
// Then
rule.check(IMPORTED_CLASSES);
}
Prevent the field and setter-based.
@Test
void testNoFieldInjection() {
// Given: Define a rule that disallows field injection using @Autowired.
ArchRule noFieldInjectionRule = noFields()
.should().beAnnotatedWith(Autowired.class)
.because("Use constructor injection instead of field injection.");
// Also define a rule that disallows setter injection using @Autowired.
ArchRule noSetterInjectionRule = noMethods()
.that().haveNameMatching("set[A-Z].*")
.should().beAnnotatedWith(Autowired.class)
.because("Use constructor injection instead of setter injection.");
// When: Combine both rules into one composite rule.
ArchRule compositeRule = CompositeArchRule.of(noFieldInjectionRule).and(noSetterInjectionRule);
// Then
compositeRule.check(IMPORTED_CLASSES);
}
Ensure the @Transactional annotation is used only in the service layer.
@Test
void testTransactionalAnnotationOnlyInService() {
// Given: Define a rule that ensures classes annotated with @Transactional
// are located in the service layer.
ArchRule classLevelTransactional = classes()
.that().areAnnotatedWith(Transactional.class)
.should().resideInAPackage("..service..")
.because("Class-level @Transactional belongs in the service layer only.");
// Also define a rule for methods annotated with @Transactional
// to be declared only in service layer classes.
ArchRule methodLevelTransactional = methods()
.that().areAnnotatedWith(Transactional.class)
.should().beDeclaredInClassesThat().resideInAPackage("..service..")
.because("Method-level @Transactional belongs in the service layer only.");
// When: Combine both rules into one composite rule.
ArchRule compositeRule = CompositeArchRule.of(classLevelTransactional).and(methodLevelTransactional);
// Then
compositeRule.check(IMPORTED_CLASSES);
}
Enforce @Repository and @Service annotation usage in specific packages.
@Test
void testRepositoryAnnotationInRepositoryPackage() {
// Given: Define a rule that ensures @Repository-annotated classes
// are only located in the repository package.
ArchRule rule = classes()
.that().areAnnotatedWith(Repository.class)
.should().resideInAPackage("..repository..");
// Then
rule.check(IMPORTED_CLASSES);
}
@Test
void testServiceAnnotationInServicePackage() {
// Given: Define a rule that ensures @Service-annotated classes
// are only located in the service package.
ArchRule rule = classes()
.that().areAnnotatedWith(Service.class)
.should().resideInAPackage("..service..");
// Then
rule.check(IMPORTED_CLASSES);
}
ArchUnit and Hexagonal Architecture
Here, the setup is somewhat more complex. A complete set of ArchUnit tests for hexagonal architecture.
Due to the Java packaging model, enforcing proper classes and methods visibility is sometimes impossible. Thus, someone may easily use the class outside its intended scope. By extending, we break the encapsulation and our beautiful separation of domain and infrastructure.
Let’s consider the following setup:
org.ps
├─ domain
│ └─ ... (domain models and services)
├─ application
│ ├─ port
│ │ ├─ in
│ │ │ └─ ... (interfaces for incoming incoming requests and messages)
│ │ └─ out
│ │ └─ ... (interfaces for outgoing requests and messages)
│ └─ ... (application services, use case implementations)
├─ adapters
│ ├─ in (incoming requests and messages)
│ └─ out (outgoing requests and messages)
├─ infrastructure
│ └─ ... (external setups - DB connections, queues, metrics)
└─ config
└─ ... (configurations classes for all other packages)
This is the closest to recommended package structure for hexagonal architecture I managed to get. It seems that there are no one general way. Almost every article is pushing its one version.
We want our structure to obey the following set of rules, as far as I have managed to understand the industry-wide standard when it comes to hexagonal architecture:
- The domain may not access any layer, but can be accessed by the Application and Adapters layers.
- Application may access the Config and Domain layers, but can be accessed by the Adapters layer.
- Adapters may access Application, Adapters, Domain, and Infrastructure, but cannot be accessed by other layers.
- Infrastructure can only access the Config layer, but can be accessed only by Adapters.
- Config may not be accessed at any layer, but can be accessed by Application, Adapters, Domain, and Infrastructure.
Below is how we may test and enforce it with the help of ArchUnit. The test is quite lengthy, but the particular rules are clearly split from one another.
@Test
public void hexagonArchTest() {
// Given
JavaClasses importedClasses = new ClassFileImporter().importPackages("org.ps.hexagon");
LayeredArchitecture portsAndAdaptersLayers = layeredArchitecture()
.consideringOnlyDependenciesInLayers()
// Define each “layer” by its package
.layer("Adapters").definedBy("..adapters..")
.layer("Application").definedBy("..application..")
.layer("Config").definedBy("..config..")
.layer("Domain").definedBy("..domain..")
.layer("Infrastructure").definedBy("..infrastructure..")
// Domain may not access any layer but can be access by Application and Adapters layers.
.whereLayer("Domain").mayNotAccessAnyLayer()
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Adapters")
// Application may access the Config and Domain layers but can be access by Adapters layer.
.whereLayer("Application").mayOnlyAccessLayers("Config", "Domain")
.whereLayer("Application").mayOnlyBeAccessedByLayers("Adapters")
// Adapters may access Application, Adapters, Domain and Infrastructure but cannot be access by other layers.
.whereLayer("Adapters").mayOnlyAccessLayers("Infrastructure", "Config", "Application", "Domain")
.whereLayer("Adapters").mayNotBeAccessedByAnyLayer()
// Infrastructure can only access Config layer but can be access only by Adapters.
.whereLayer("Infrastructure").mayOnlyAccessLayers("Config")
.whereLayer("Infrastructure").mayOnlyBeAccessedByLayers("Adapters")
// Config may not be access any layer but can be access by Application, Adapters, Domain and Infrastructure.
.whereLayer("Config").mayNotAccessAnyLayer()
.whereLayer("Config").mayOnlyBeAccessedByLayers("Application", "Adapters", "Domain", "Infrastructure");
// Then
portsAndAdaptersLayers.check(importedClasses);
}
Summary
Here we are, that is all I wanted to share with you today. If you want some more examples, you can find them either on the ArchUnit GitHub or in their docs.
On the other hand, you can just start typing and see where the API will guide you.
Thank you for your time.
Published at DZone with permission of Bartłomiej Żyliński. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments