Architecture and Design — Unit Testing using ArchUnit
By: George Daniel Dragomir
Tags: Best Practices, Architecture, Programming Languages, Testing Tools

Most of the projects in their beginning phase have a well-written documentation that reflects the reality (the code).

As the projects evolve and new developers join, the well-written starting/opening documentation falls behind and becomes obsolete.

Wouldn’t be more useful to have this code-level documentation put together as unit tests? This is exactly what we are going to do here with ArchUnit.

This way, creating and maintaining the architecture will become a team sport for all programmers of the project.

Also, each new member of the team will understand and respect in an easier manner the project’s architecture, without having to read the fragmented, incomplete and obsolete documentation.

1. What is software architecture?

It is a very broad term and there are a lot of different definitions for architecture:

The design decisions that need to be made early in a project

Fundamental structural choices that are costly to change once implemented

“The shared understanding that the expert developers have of the system design” — Ralph Johnson

2. Why does the architecture matter?

Architecture is a tricky subject for the product owners and users of software products. It isn’t something that they immediately perceive, but a poor architecture is a major contributor to the growth of cruft.

Software that contains a lot of cruft is much harder to modify, leading to features that arrive more slowly and with more defects.

It is true that we can sacrifice quality for faster delivery in the short term, before the build up of cruft has an impact, but people underestimate how quickly the cruft leads to an overall slower delivery.

3. How does good architecture look like?

The architecture is into a continuous evolution and it depends on the moment in time when we are looking at it.

“A good software architecture is something that supports its own evolution, and is deeply intertwined with programming” — Martin Fowler

4. Software architecture styles

Nowadays the most important architecture styles for applications and APIs are:

4.1. Layered architecture / Multi-tier architecture

There are two important rules for a classical Layered Architecture to be correctly implemented:

All the dependencies go in one direction, from the presentation layer to infrastructure (top/down)

No logic related to one layer’s concern should be placed in another layer

4.2. Hexagonal architecture (Ports and Adapters architecture)

It divides the application into the inside and the outside parts:

In the inside part is the core logic of the application

In the outside part could be the UI, database, messaging connectors, etc.

The application’s business logic is isolated from outside concerns.

Communication between the two happens using so-called ports and adapters.

The port is simply a gateway to your application, allowing inbound and outbound flow.

Adapters are an implementation of ports.

4.3. Microservice architecture

Is a modularization approach that aims at dividing larger software systems into smaller components.

With a microservices architecture, an application is built with an independent group of components that run each application process as a service.

5. Architecture documentation

Why do we need architecture documentation?

Documentation is the starting point for interaction between different stakeholders

It is used for analyses and reviews of the project

Knowledge sharing — between people working in different functional areas of the project, as well for knowledge transfer to new programmers added into the project

When it is worth starting to create and maintain a documentation?

It is worth creating and maintaining a proper and updated documentation taking into consideration that the cost of the project with documentation plus the cost of maintaining that documentation is less than the cost of the project without this documentation.

There could be cases when the projects are very small and the cost of writing a documentation is greater than the actual development of the project.

How is the architecture documented?

The architecture can be documented using Confluence pages, diagrams or unit tests.

An example of an architecture diagram is the C4 model:

A tool that could help us to write architecture unit tests is ArchUnit:

What is ArchUnit?

ArchUnit is a free, simple and extensible library for checking the architecture of your Java Code.

It was originally created by Peter Gafert and in this moment the ArchUnit is mainly developed by TNG.

It can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more.

How does it work?

It is analyzing the given Java bytecode, importing all classes into a Java code structure.

ArchUnit’s main focus is to automatically test architecture and coding rules, using any plain Java unit testing framework.

Documentation

ArchUnit documentation can be found at: https://www.archunit.org/userguide/html/000_Index.html

Installation

To use ArchUnit, it is sufficient to include the respective JAR files in the classpath.

Most commonly, this is done by adding the dependency to your dependency management tool.

<!-- https://mvnrepository.com/artifact/com.tngtech.archunit/archunit-junit4 -->

<dependency>

<groupId>com.tngtech.archunit</groupId>

<artifactId>archunit-junit4</artifactId>

<version>0.14.1</version>

<scope>test</scope>

</dependency>

Writing architecture tests

For example, if you want to write a rule that enforces services to be called only from controllers or other services you should follow the steps:

Import the classes you what to test into the Java code structures:

JavaClasses importedClasses = new ClassFileImporter().importPackages("com.demo.product");

2. Rule conditions

ArchRule rule = classes()

.that().resideInAPackage("..service..")

.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

3. Check the rule against the imported classes:

rule.check(importedClasses);

The overall example:

@Test

public void servicesShouldOnlyBeAccessedByControllersOrServices() {



JavaClasses importedClasses = new ClassFileImporter().importPackages("com.demo.product");



ArchRule rule = classes()

.that().resideInAPackage("..service..")

.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");



rule.check(importedClasses);

}

If this test fails, it will report an AssertionError with the following message:

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package '..service..' should only be accessed by any package ['..controller..', '..service..']' was violated (1 times):

Method <some.packge.name.CallerClass.callerMethod()> calls method <some.package.name.service.ClassService.calledMethod()> in (CallerClass.java:33)

For more examples you can access:

Official ArchUnit examples: https://github.com/TNG/ArchUnit-Examples

Demo project written by me: https://github.com/g-dragomir/archunit-demo

Why should you use ArchUnit?