Jeremy Cook
Unit test your Java Architecture
with ArchUnit
What, why and how to unit test your architecture
Agenda
1. What is ArchUnit?

2. Why do I want to test my architecture?

3. ArchUnit overview

4. Limitations
ArchUnit Website
“ArchUnit is a free, simple and extensible library
for checking the architecture of your Java
code using any plain Java unit test framework”
Why do I want to test my
architecture?
Problems architects face
Architecture is intangible

Knowing the design is implemented

Systems tend towards entropy over time

Architectural erosion ➡ loss of architectural characteristics
Fitness functions can help
Building Evolutionary Architectures by Neal Ford, Rebecca Parsons and Patrick Kua
“An architectural fitness function provides
objective integrity of some architectural
characteristic(s)”
Architectural 

Characteristics
Performance
Scalability
Durability
Accessibility
Fault tolerance
Elasticity
Stability
Evolvability
Maintainability
Comprehensibility
Testability
Verifiable with 

ArchUnit
*Not an exhaustive list
Architectural 

Characteristics*
ArchUnit allows fitness functions to be
created that verify and protect architectural
characteristics expressed in code
How ArchUnit helps
Architecture as code ➡ tangible architecture

Architecture violations ➡ build failures

Harder to unintentionally change design
Verifiable with 

Static Analysis
Verifiable with

ArchUnit
Verifiable with 

Static Analysis
Verifiable with

ArchUnit
ArchUnit overview
Anatomy of an ArchUnit test
1. Find code to verify

2. Create one or more rules

3. Check code against rules
private final JavaClasses classes = new ClassFileImporter()
.importPackages(“com.myapp.somepackage”, “com.myapp.other”);
ArchRule rule = classes().that().resideInAPackage(“..service..”)
.should().onlyHaveDependentClassesThat()
.resideInAnyPackage("..controller..", “..service..");
private final JavaClasses classes = new ClassFileImporter()
.importPackages(“com.myapp.somepackage”, “com.myapp.other”);
@Test
public void checkServiceDependencies() {
}
classes().that().resideInAPackage(“..service..”)
.should().onlyHaveDependentClassesThat()
.resideInAnyPackage("..controller..", “..service..”)
.check(classes);
Identifying code to test
Using ClassFileImporter
Import by class, classpath, JAR, location, package name, packages of class(es),
URL and path

Resolves dependencies of imported code

Filter imported code by location
private final JavaClasses classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(location -> !location.contains("foo"))
.withImportOption(location -> location.matches(Pattern.compile(“.*“)))
.importClasspath();
Working with Rules
ArchUnit rules
CODE UNITS (classes, methods, fields, constructors, code units, etc)

THAT meet one or more conditions (optional)

SHOULD have one or more architectural characteristics
noClasses().that().areInterfaces()
.should().haveSimpleNameContaining(“Interface”)
.check(classes);
theClass(VeryCentralCore.class)
.should().onlyBeAccessed()
.byClassesThat().implement(CoreSatellite.class)
.check(classes);
fields().that().haveRawType(Logger.class)
.should().bePrivate()
.andShould().beStatic()
.andShould().beFinal()
.check(classes);
constructors().that()
.areDeclaredInClassesThat().resideInAPackage("..controller..")
.should().beAnnotatedWith(Inject.class)
.check(classes);
noMethods().that()
.areDeclaredInClassesThat().haveNameMatching(".*Dao")
.should().declareThrowableOfType(SQLException.class)
.check(classes);
.should().haveRawReturnType(Optional.class)
.orShould().beAnnotatedWith(NotNull.class)
.check(classes);
methods().that().arePublic()
.should().haveRawReturnType(Optional.class)
.orShould().beAnnotatedWith(NotNull.class)
.check(classes);
methods().that().arePublic()
.and().doNotHaveRawReturnType("void")
.should().haveRawReturnType(Optional.class)
.orShould().beAnnotatedWith(NotNull.class)
.check(classes);
methods().that().arePublic()
.and().doNotHaveRawReturnType("void")
.and().areDeclaredInClassesThat()
.areNotAnnotatedWith(ParametersAreNonnullByDefault.class)
.and(GET_RAW_RETURN_TYPE.is(not(assignableTo(Collection.class))))
.and(GET_RAW_RETURN_TYPE.is(not(assignableTo(Map.class))))
.should().haveRawReturnType(Optional.class)
.orShould().beAnnotatedWith(NotNull.class)
.check(classes);
methods().that().arePublic()
.and().doNotHaveRawReturnType("void")
.and().areDeclaredInClassesThat()
.areNotAnnotatedWith(ParametersAreNonnullByDefault.class)
@Test
public void controllersMustBeAnnotatedWithRestController() {
}
classes().that().resideInAPackage(“..controller..")
.should().beAnnotatedWith(RestController.class)
.check(classes);
@Test
public void onlyControllersHaveRestControllerAnnotation() {
}
noClasses().that().resideOutsideOfPackage("..controller..")
.should().beAnnotatedWith(RestController.class)
.check(classes);
@Test
public void publicMethodsInControllersMustHaveRouteAnnotation() {
}
methods().that()
.areDeclaredInClassesThat().resideInAPackage("..controller..")
.and().arePublic()
.should().beAnnotatedWith(GetMapping.class)
.orShould().beAnnotatedWith(PostMapping.class)
.orShould().beAnnotatedWith(DeleteMapping.class)
.orShould().beAnnotatedWith(PutMapping.class)
.check(classes);
Additional Features
Customizing failure messages
ArchUnit uses method names in rules for error messages

Customizable in two ways:

• Append text with .because()

• Replace error message with.as()
@Test
public void publicStaticFieldsShouldBeFinal() {
}
fields().that().arePublic()
.and().areStatic()
.should().beFinal()
.check(classes);
@Test
public void publicStaticFieldsShouldBeFinal() {
}
fields().that().arePublic()
.and().areStatic()
.should().beFinal()
.check(classes);
@Test
public void publicStaticFieldsShouldBeFinal() {
}
fields().that().arePublic()
.and().areStatic()
.should().beFinal()
.because("mutable public state is not a good idea")
.check(classes);
@Test
public void publicStaticFieldsShouldBeFinal() {
}
fields().that().arePublic()
.and().areStatic()
.should().beFinal()
.because("mutable public state is not a good idea")
.check(classes);
@Test
public void publicStaticFieldsShouldBeFinal() {
}
fields().that().arePublic()
.and().areStatic()
.should().beFinal()
.as("Don't give public fields mutable state")
.check(classes);
@Test
public void publicStaticFieldsShouldBeFinal() {
}
fields().that().arePublic()
.and().areStatic()
.should().beFinal()
.as("Don't give public fields mutable state")
.check(classes);
Creating custom rules
CODE UNITS (classes, methods, fields, constructors, code units, etc)

THAT meet one or more conditions

SHOULD have one or more architectural characteristics
Creating custom rules
CODE UNITS (classes, methods, fields, constructors, code units, etc)

DESCRIBED PREDICATES {THAT meet one or more conditions}

SHOULD have one or more architectural characteristics
Creating custom rules
CODE UNITS (classes, methods, fields, constructors, code units, etc)

DESCRIBED PREDICATES {THAT meet one or more conditions}
ARCH CONDITIONS {SHOULD have one or more architectural characteristics}
Creating custom rules
Create DescribedPredicates and ArchConditions in two ways:

1. Compose using built in library functions

2. Extend to create custom classes
.and(GET_RAW_RETURN_TYPE.is(not(assignableTo(Collection.class))))
.and(GET_RAW_RETURN_TYPE.is(not(assignableTo(Map.class))))
.should().haveRawReturnType(Optional.class)
.orShould().beAnnotatedWith(NotNull.class)
.check(classes);
methods().that().arePublic()
.and().doNotHaveRawReturnType("void")
.and().areDeclaredInClassesThat()
.areNotAnnotatedWith(ParametersAreNonnullByDefault.class)
classes().that().areAssignableTo(Serializable.class)
.and().areNotEnums()
.and().areNotInterfaces()
.should(new HaveAValidSerialVersionUIDField())
.check(classes);
public class HaveAValidSerialVersionUIDField extends ArchCondition<JavaClass> {
}
public class HaveAValidSerialVersionUIDField extends ArchCondition<JavaClass> {
}
public HaveAValidSerialVersionUIDField() {
super("have a valid serialVersionUID field");
}
public class HaveAValidSerialVersionUIDField extends ArchCondition<JavaClass> {
}
public HaveAValidSerialVersionUIDField() {
super("have a valid serialVersionUID field");
}
@Override
public void check(JavaClass item, ConditionEvents events) {
}
public class HaveAValidSerialVersionUIDField extends ArchCondition<JavaClass> {
}
public HaveAValidSerialVersionUIDField() {
super("have a valid serialVersionUID field");
}
@Override
public void check(JavaClass item, ConditionEvents events) {
}
var errorMessage = item.getName() + " does not contain a valid serialVersionUID field";
try {
JavaField field = item.getField("serialVersionUID");
} catch (IllegalArgumentException e) {
events.add(SimpleConditionEvent.violated(item, errorMessage));
}
public class HaveAValidSerialVersionUIDField extends ArchCondition<JavaClass> {
}
public HaveAValidSerialVersionUIDField() {
super("have a valid serialVersionUID field");
}
@Override
public void check(JavaClass item, ConditionEvents events) {
}
var errorMessage = item.getName() + " does not contain a valid serialVersionUID field";
try {
JavaField field = item.getField("serialVersionUID");
} catch (IllegalArgumentException e) {
events.add(SimpleConditionEvent.violated(item, errorMessage));
}
var hasValidSerialVersionUID =
HasModifiers.Predicates.modifier(JavaModifier.STATIC).apply(field)
&& HasModifiers.Predicates.modifier(JavaModifier.FINAL).apply(field)
&& HasType.Predicates.rawType("long").apply(field);
events.add(new SimpleConditionEvent(item, hasValidSerialVersionUID, errorMessage));
General Coding Rules
Small number of pre-configured rules to test common conditions
GeneralCodingRules.NO_CLASSES_SHOULD_USE_FIELD_INJECTION
.check(classes);
GeneralCodingRules.NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS
.check(classes);
Testing architectural layers
Can be done manually

Three specialized rule types for checking layers:

• Check lower packages do not depend on upper packages

• Define layers and check dependencies between them

• Testing onion architectures
DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES
.check(classes);
layeredArchitecture()
.layer(“Controllers")
.definedBy(“com.myapp.somepackage.controller..")
.layer(“Services")
.definedBy("com.myapp.somepackage.service..")
.layer(“Persistence")
.definedBy(“com.myapp.somepackage.persistence..")
.whereLayer(“Controllers")
.mayNotBeAccessedByAnyLayer()
.whereLayer(“Services")
.mayOnlyBeAccessedByLayers("Controllers")
.whereLayer(“Persistence")
.mayOnlyBeAccessedByLayers("Services")
.ignoreDependency(SomeMediator.class, ServiceViolatingLayerRules.class)
.check(classes);
onionArchitecture()
.domainModels("..domain.model..")
.domainServices("..domain.service..")
.applicationServices("..application..")
.adapter("cli", "..adapter.cli..")
.adapter("persistence", "..adapter.persistence..")
.adapter("rest", "..adapter.rest..")
.check(classes);
Adding ArchUnit to existing codebases
How can you add architecture tests to existing code that has violations?
1. Ignoring violations based on patterns

2. Freezing architecture rules
var rule = fields().that().arePublic()
.and().areStatic()
.should().beFinal();
FreezingArchRule.freeze(rule)
.check(classes);
Other features
• Check code against PlantUML diagrams

• Options to manage dependencies outside of diagram

• Identify and test slices of an application:

• Check for cycles

• Check slices do not depend on each other
Limitations
Cannot test all architectural
characteristics
Does not ensure maintainability
on its own
Can only check (some) JVM
languages
Importing large amounts of code
More information
ArchUnit website: https://www.archunit.org

Sample project: https://github.com/TNG/ArchUnit-Examples
Questions?
Thank you
Feel free to reach out to me

• Twitter @JCook21

• jeremycook0@icloud.com

Unit test your java architecture with ArchUnit