Unit test your java architecture with ArchUnit

jeremycook0 618 views 43 slides Feb 25, 2021
Slide 1
Slide 1 of 76
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51
Slide 52
52
Slide 53
53
Slide 54
54
Slide 55
55
Slide 56
56
Slide 57
57
Slide 58
58
Slide 59
59
Slide 60
60
Slide 61
61
Slide 62
62
Slide 63
63
Slide 64
64
Slide 65
65
Slide 66
66
Slide 67
67
Slide 68
68
Slide 69
69
Slide 70
70
Slide 71
71
Slide 72
72
Slide 73
73
Slide 74
74
Slide 75
75
Slide 76
76

About This Presentation

From Confoo 2021.

Software architecture tends to be esoteric and intangible. The result of this is architectural drift, with the architecture losing the qualities it was promoting as the code evolves. This talk will introduce ArchUnit, a library that allows you to test your Java architecture. You&#...


Slide Content

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
[email protected]