Architecture tests with ArchUnit

Bruna Pereira
3 min readAug 3, 2020

Versão em português aqui.

It has been a while since ADRs became popular. Architecture Decision Records are used to align technical decisions and also keep the next people updated on the motivation of past decision making. This practice is used in a code base, team or even area level.

Speaking specifically of the code base, developers should know that it is very difficult to keep documentation up to date. In fact, this phenomenon of difficulty in updating documentation should be studied, because it is a recurring problem worldwide 😛. But until we get rid of this problem, a very interesting way to ensure that decision making regarding the architecture / design pattern of the code is not overlooked and enforced, is to use what we do best: automate.

ArchUnit is a tool for creating automated tests for code architecture. With ArchUnit you can write your architectural decisions in the form of a test and integrate them into the deploy pipeline just like any other automated test you have in your application. The documentation is very complete, and besides, their API is very intuitive

What to test?

The most notable functionalities in the documentation are:

  • Check cyclical dependencies;
  • Ensure a layered architecture;
  • Check dependency between packages;
  • Check class dependencies;
  • Among others.

An interesting feature is to test whether your code adheres to an UML diagram. You can decide with your team which layers communicate, create a diagram, import it and use it in your test, as described here in the documentation.

Examples

The first time I used ArchUnit was on a very large monolith, with a team of many people, and a codebase without standards. We started with the basics:

  1. Ensure that the components live in the corresponding package:
@ArchTest
val controllersMustResideInViewPackage: ArchRule =
classes().that().haveSimpleNameEndingWith("Controller")
.should().resideInAPackage("..view..")

2. Ensure that we don’t have database entities scattered around the code:

@ArchTest
val entitiesMustResideInARepositoryPackage: ArchRule =
classes().that().areAnnotatedWith(Entity::class.java)
.should().resideInAPackage("..infrastructure.jpa..")

3. Ensure thata we don’t have classes with the suffix DTO:

@ArchTest
val classesMustNotBeSuffixedWithDto: ArchRule =
noClasses().should().haveSimpleNameEndingWith("Dto")
.orShould().haveSimpleNameEndingWith("DTO")

4. Ensure that communication restrictions between different layers are being respected:

@ArchTest
val viewLayerDoesNotAccessInfrastructureLayer: ArchRule =
noClasses().that().resideInAPackage("..view..")
.should().accessClassesThat()
.resideInAPackage("..infrastructure..")

Tips

You don’t have to refactor all of your code to comply with standards every time they are born. You can use the Freezing functionality.
Basically what Freezing does is create an initial threshold of violations at the time of creating the test, and if the number of violations increases, the test fails. If it decreases, the acceptable number of violations also decreases.

Another tip is to separate the architecture test suite from the unit test suite. These tests are needed much less often, and there is no need to add that extra run time each time you run the tests during development.

To separate my test suites, I use a gradle plugin called test-sets and with a few lines of code I separate a package for my architecture tests that has its own gradle command to run. In addition, all tests are performed in a simple gradle check.

Conclusion

There are a number of other more advanced features of ArchUnit, but as their documentation says, you can start testing your architecture in 30 minutes. Don’t wait to test more complex solutions, start with the basics and engage your team to contribute.

Special thanks to Maniero, Conca, Gabi Mattos and Kael who were together in the discovery of the tool. ❤️

--

--