Automated tests provide fast, frequent and early feedback when code changes are made. Historically we’ve talked about unit tests, integration tests and acceptance tests, looking at increasing levels of code to business functionality. What if we could apply the same concepts to our architecture?
Enter stage left ArchUnit - a Java testing library that inspects your code’s architecture. Assertions can be made on your code’s structure, properties and relationships to provide further automated quality guarantees about the health of your codebase.
For the purposes of this article, I’m going to be using ArchUnit with Junit 5. Other test frameworks are compatible and available. I’ll explain concepts as we go along.
Firstly, add the following test-scoped dependencies into your project’s POM:
Now, it’s time to write our test class.
AnalyzeClasses annotation sets up ArchUnit to load classes in the specified package. ArchUnit inspects the compiled bytecode and builds it into an analyzable data structure for usage within your tests. I’ve specified some customisation rules for loading classes to prevent the inclusion of third-party JARs and test classes.
From here, individual architectural tests are tagged with the
@ArchTest annotation, similarly to how traditional JUnit tests are tagged with
I’d also recommend adding the following
logback-test.xml configuration, to prevent a huge amount of DEBUG statements from cluttering your log files:
The rule can always be switched off if you fancy getting into the weeds of how the library initialises and operates.
Including Pre-Configured Tests
ArchUnit ships with a set of includable tests that cover common use cases. The following snippet demonstrates two of these - enforcing that we throw meaningful (rather than generic) exceptions, and that we don’t use JodaTime. I’ve worked in repositories that have combined JodaTime and Java Time. It gets messy quick. Let’s enforce it rather than argue about it!
There are further common coding rules that you can include, which prevents
System.out calls, ensures the correct usage of a logger and so on.
Defining Architectural Boundaries
Let’s envisage an application with a traditional 3-layered architecture:
webpackage - defines Spring MVC
dtopackage - defines API Request/Response objects
servicepackage - defines services that perform business transactions
domainpackage - framework-independent object modelling of our business domain
entitypackage - defines Object-Relational Mapping entities
repositorypackage - for querying and data access
Good architectural practice ensures that we have a clear separation of responsibilities between the layers. Returning Hibernate entities from our API endpoints should be discouraged. Instead, prefer the usage of separate DTOs to prevent tight-coupling to your database schema. Likewise, the core business domain is expressed by a set of domain objects that remain independent of both. Sure, in a small sample project there’s a quite a bit of duplication, but as an applications grows these will have separate concerns.
ArchUnit has out-of-the-box functionality to assert that your layered architecture is respected. Define layers by specifying the corresponding packages and then express a network of permitted dependencies between these layers. This test then provides automated assurances that access and usage is maintained according to your defined boundaries:
If you follow a Ports and Adapters (Hexagonal Architecture, Onion Architecture) approach, don’t worry. ArchUnit provides assertions according to the terminology and dependency structures outlined here.
Asserting Classes are in the Correct Package
The test above is fantastic for making quite sweeping assertions. It clearly communicates the design decision and acts as the source of truth. However it’s easy to avoid - simply put a different type of class in a different package and use it the wrong way. Naughty, devious, nefarious! So we’ll want to defend against that too.
ArchUnit provides a fluent DSL which lets you attach conditions to a given situation. This lets us assert that classes that have particular annotations must belong to particular packages, or that they have particular names. By doing so, we’re establishing our coding conventions in a form that’s easy to test!
It’s also possible to encode your decisions on particular discussions that largely come down to personal preference. Don’t like using the
Impl suffix for interface implementations? Enforce it with a test! As an added extra, the
because block lets you go into detail about your choice. Or to be passive-aggressive.
Other static analysis tools exist on the marketplace. Some are more declarative compared to the programmatic nature of ArchUnit. The power of this library comes, in my opinion, from its fluent expressions and its extensibility. By being a Java library, the language you’re already familiar with, you benefit from the additional tooling capabilities and leads to an overall lower cost of maintenance.
As a tool which helps makes your knowledge of the system’s operation explicit instead of implicit, ArchUnit also aids new developers who join the project at a later date. Architectural decisions are defined in code and enforced in an automated manner. Rather than relying on hearsay or reading a wiki page to establish conventions, look at the tests. They’re a form of executable documentation.
It also helps you stay on the straight and narrow. There are plenty of times where a corner’s cut or a rule is violated and subsequently not spotted at code review. This risks introducing a bug or incurring tech debt. We’re humans. I forgive you. But by encoding these choices in an executable form we can provide fast feedback on whether changes satisfy our architectural boundaries, lowering the cost of correcting such mistakes. Also, it’s one less thing to review manually.
ArchUnit has a comprehensive user guide detailing the core concepts, alongside a treasure trove of examples covering a myriad of use cases. Suffice to say I’ve merely scratched the surface in this article.
In summary, this is a fascinating tool that can aid keeping a codebase clean, maintainable and pleasant to work in. It’s definitely worth investing in a solution that strengthens your game in this area.