Testing the Architecture: ArchUnit in Practice
While many of the architectural challenges we have to deal with are big hard choices, there are also many smaller simpler ones.
From "don’t call repository classes from controllers" to "don’t have cyclic dependencies". In most projects I’ve worked on these are unwritten rules. But why not write them down in a way that we can also see if the rules get broken? Can we test these rules?
Introduction
In my opinion one of the most important components of a mature software engineering process is to have a peer review process in place. One of the complaints you often hear about peer reviews is the time they take. This is often seen as 'waste'. Often these complaints have a very good point; it’s easy to end up in a situation where reviews end up as bike-shedding exercises while they should focus on learning and making the software better.
So an important aspect is to automate these checks as much as possible. Linters like CheckStyle and FindBugs are in my opinions mandatory to get rid of most of the uncertainty in a code review. But these kind of Linters can often not check more complex rules. Fortunately there is a neat tool for these rules: ArchUnit.
What you can achieve with ArchUnit is having an automated set of architectural tests you can run against any code-base automatically, that will inform you (in an as unsubtle manner as you want) that someone is violating the rules you as a group decided up-on. This way you can be sure that you won’t have some teams drifting off in a different direction architecture wise and focus on the things that matter in your peer reviews.
ArchUnit
ArchUnit is a testing library that aims to, as opposed to testing the business logic of your code (what you’d normally write unit or integration tests for), test the code to see if it still fits predefined architectural rules. Of from the ArchUnit page itself:
ArchUnit is a free, simple and extensible library for checking the architecture of your Java code using any plain Java unit test framework. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more. It does so by analyzing given Java bytecode, importing all classes into a Java code structure.
It plugs into your existing test framework. In my example I will use JUnit 5 as the runner.
I have created an example microservice project with a "Foo" service and a "Bar" service. Because we love building abstractions we have both services inherit from a Service Base that acts as our own miniature web framework.
For demonstration purposes these services are in their own separate Maven modules. In practice you would probably have these micro services in their own separate repositories, which works fine too. I will give some examples of rules you can implement with ArchUnit. Whether they fit your architecture or are even a good idea I will leave up to you. The main goal here is to see the power (and shortcomings) of ArchUnit.
The tests are all in the ArchitectureTest class in the test module. This demonstrates that, since ArchUnit works with bytecode inspection, it does not have to be inside the module you’re testing. It is a 'meta' test set that can be used on all your projects.
All the tests are currently green in the project. By all means go ahead and clone the project and try 'breaking' the tests by changing the code so the architectural rules are broken!
Setup
First things first though. To be able to scan the services we will need to put their jars on the classpath (which is handled by maven). We also need to set up a set of classes we want to scan against, to make sure we don’t break our tests with Test code or external dependencies:
private JavaClasses classes;
@BeforeEach
public void setup() {
classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DONT_INCLUDE_TESTS)
.importPackages("example");
}
Limit Class Accessibility
With the first example I will show a solution for a common pattern when building services. You have different 'tiers' in your application (generally three; Controllers, Services and Repositories) and you want to make sure that they are only accessed by the layers above. You can do this very easily in ArchUnit:
@Test
public void serviceClassesShouldOnlyBeAccessedByApplication() {
classes()
.that().resideInAPackage("example.*service.service")
.should().onlyBeAccessed().byAnyPackage("example.*service")
.check(classes);
}
-
Work on classes that reside in the
example.*service.service
packages -
should only be accessed by packages in the
example.*service
package -
Check the rule against the classes we have imported in the
@BeforeEach
You can try breaking this easily by for example have FooRepository refer to a FooService by adding this line to FooRepository:
private FooService fooService = new FooService(null);
Naming of Service Classes
It’s quite a common rule that you name all service classes <Something>Service, this is also something you can easily do with arch-unit:
@Test
public void serviceClassesShouldBeNamedXService() {
classes()
.that().resideInAPackage("example.*service.service")
.should().haveSimpleNameEndingWith("Service")
.check(classes);
}
Adding a HelperComponent in a .service package next to FooService for example will break this test. But what if we want to allow both Component and Service classes in that package? Well, we can agree together that this is okay and change the rule to:
classes()
.that().resideInAPackage("example.*service.service")
.should().haveSimpleNameEndingWith("Service")
.orShould().haveSimpleNameEndingWith("Component")
.check(classes);
Now you can have both *Service and *Component classes in the .service package.
Testing for Inheritance
Because we suffer from NIH we built our own web framework that we want to build upon. So our Application classes need to extend these. We can test for this like so:
@Test
public void applicationClassShouldImplementServiceBase() {
classes()
.that().resideInAPackage("example.*service")
.should()
.beAssignableTo(ServiceBase.class)
.check(classes);
}
You can reverse this too; if you have decided to move away from your own web framework and use a standard one instead you can invert the test to see which projects are not compliant to the new architecture:
.notBeAssignableTo(ServiceBase.class)
Another example, let’s say we want our domain classes to be serializable (a common requirement when working with Spark):
@Test
public void domainClassesShouldBeSerializable() {
classes()
.that().resideInAPackage("example.*service.domain")
.should()
.beAssignableTo(Serializable.class)
.check(classes);
}
Accessibility of Classes
As architects you can decide to publish domain libraries for your services separately. You might want to make sure that these classes can actually be used and set a rule that all domain classes should be public:
@Test
public void domainClassesShouldBePublic() {
classes()
.that().resideInAPackage("example.*service.domain")
.should()
.bePublic()
.check(classes);
}
On the other hand, we might want to have a rule that certain classes, like Utility classes, should have package private access:
@Test
public void utilClassesShouldBePackagePrivate() {
classes()
.that().haveSimpleNameEndingWith("Util")
.should()
.bePackagePrivate()
.check(classes);
}
Annotation Usage
After we decided that we want to move to Spring instead of our own home grown framework, we might also want to make sure that our code actually uses the proper Spring annotations. Since Spring annotations are available at runtime they can be seen by ArchUnit. So we can create a rule where we make sure that our Repository classes actually have the Spring @Repository stereotype:
@Test
public void repositoryClassesShouldHaveSpringRepositoryAnnotation() {
classes()
.that().resideInAPackage("example.*service.repository")
.should().beAnnotatedWith(Repository.class)
.check(classes);
}
Repository classes without the annotation will fail the tests.
Working on Slices
A very powerful capability of ArchUnit is how it can work on slices. A slice is a subset of classes that can be tested against another slice. A great example of the usage is a test for a common anti-pattern in microservice architectures. We publish domain libraries (or clients) for our Foo and Bar services. Bar calls Foo. An easy pitfall would be to have Bar’s domain classes use Foo’s domain classes directly: a change to the interface of Foo would also chainge the interface of Bar.
Why is this bad? These kinds of transitive changes are hard to detect and test for. In complex architectures these dependencies can chain many layers deep. And if you test by using the packaged clients instead of testing the contract the tests will generally succeed even though you broke the contract.
So how we test this with ArchUnit: it is in fact incredibly simple:
@Test
public void domainClassesShouldNotDependOnEachOther() {
SlicesRuleDefinition.slices()
.matching("example.(*service).domain")
.should().notDependOnEachOther()
.check(classes);
}
A huge anti-pattern detected by 7 lines of code!
So how does this work? We’ve defined slices with a 'capture' on (*service)
.
This means that we now have a separate slice for fooservice and barservice (and if we’d later add bazservice it would be another slice automatically).
ArchUnit then applies these rules to the individual slices.
This test is currently green in the test project, but if you have Bar.java use Foo.java, ArchUnit would fail the test.
Creating Custom Rules
While ArchUnit have a lot of built-in functions in an excellent fluent API there are some more complex cases it can’t handle out of the box. Fortunately it offers a great extension methods where you can create almost any custom rule you can dream up. So let’s create one!
A common linter check is one to force all utility classes to have only static methods and a private constructor. While the rules we’ve seen so far apply to classes, you can also create rules that apply to fields, methods or constructors. While this is a tad more complex, the way ArchUnit implements this makes the components of the rules to be easily reusable.
So let’s create a rule that makes sure that our utility classes (classes ending on 'Util') all have private constructors. We start by creating a transformer that transforms a list of classes to a list of constructors, filtering on whether these classes end with 'Util':
ClassesTransformer<JavaConstructor> utilityConstructors =
new AbstractClassesTransformer<JavaConstructor>("utility constructors") {
@Override
public Iterable<JavaConstructor> doTransform(JavaClasses classes) {
Set<JavaConstructor> result = new HashSet<>();
for (JavaClass javaClass : classes) {
if(javaClass.getSimpleName().endsWith("Util")) {
result.addAll(javaClass.getConstructors());
}
}
return result;
}
};
This could be expressed as:
classes
.filter(c -> c.name.endsWith("Util"))
.flatMap(c -> c.getConstructors())
Unfortunately ArchUnit does not seem to support Lambda syntax for these kinds of tasks yet.
Now we also have to define a condition that checks whether these constructors have private access:
ArchCondition<JavaConstructor> havePrivateConstructors = new ArchCondition<JavaConstructor>("be private") {
@Override
public void check(JavaConstructor constructor, ConditionEvents events) {
boolean privateAccess = constructor.getModifiers().contains(PRIVATE);
String message = String.format("%s is not private", constructor.getFullName());
events.add(new SimpleConditionEvent(constructor, privateAccess, message));
}
};
This maps every JavaConstructor to a ConditionEvents. The event contains a reference to the constructor, whether the condition is fulfilled, and a readable message.
We can now tie this filter-flatmap-map together into a rule:
all(utilityConstructors).should(havePrivateConstructors).check(classes);
Changing the constructor in ServiceUtil to public will trigger this rule.
Further Examples
I added a few more tests to the example tests, for example to prevent cycles or check whether utility functions are public and static. The ArchUnit project itself also has tons of examples. Check them out!
Limitations of ArchUnit
First of all; ArchUnit works by analysing the compiled byte code produces by the Java (or Kotlin) compiler.
Anything not available at runtime won’t be visible to ArchUnit.
An example are lombok annotations.
Since lombok’s @Data
annotation is not runtime available it is not possible to test for it.
Instead you get test if data classes only have "get" and "set" methods, or if all fields are final.
Secondly; while ArchUnit works conveniently on sets of classes, it is somewhat more cumbersome to check methods. That’s definitely an area where improvements to the library could be made. This can be circumvented by creating your own set of custom rules as I demonstrated in the last example. It’s not hard to build your own set of reusable test components due to the pluggable nature of the ArchUnit API.
And last; it would be awesome if ArchUnit could support Java 8 syntax better for custom rules!
Conclusion
ArchUnit is an excellent tool to automatically test architectural rules that are easily missed. While at the start of a project you often all agree on a set of these guidelines, over time with new hires and growing teams it can become hard for everyone working on your code to conform to the same guidelines.
While many coding guidelines (tabs vs. spaces, naming of members) can be handled by linters such as CheckStyle, ArchUnit with it’s powerful byte code inspection and excellent fluent API can be a great additional to a software architect’s toolset.