mscharhag, Programming and Stuff;

A blog about programming and software development topics, mostly focused on Java technologies including Java EE, Spring and Grails.

Saturday, 1 February, 2020

Validating code and architecture constraints with ArchUnit

Introduction

ArchUnit is a library for checking Java code against a set of self defined code and architecture constraints. These constraints can be defined in a fluent Java API within unit tests. ArchUnit can be used to validate dependencies between classes or layers, to check for cyclic dependencies and much more. In this post we will create some example rules to see how we can benefit from ArchUnit.

Required dependency

To use ArchUnit we need to add the following dependency to our project:

<dependency>
	<groupId>com.tngtech.archunit</groupId>
	<artifactId>archunit-junit5</artifactId>
	<version>0.13.0</version>
	<scope>test</scope>
</dependency>

If you are still using JUnit 4 you should use the archunit-junit4 artifact instead.

Creating the first ArchUnit rule

Now we can start creating our first ArchUnit rule. For this we create a new class in our test folder:

@RunWith(ArchUnitRunner.class) //only for JUnit 4, not needed with JUnit 5
@AnalyzeClasses(packages = "com.mscharhag.archunit")
public class ArchUnitTest {

    // verify that classes whose name name ends with "Service" should be located in a "service" package
    @ArchTest
    private final ArchRule services_are_located_in_service_package = classes()
            .that().haveSimpleNameEndingWith("Service")
            .should().resideInAPackage("..service");
}

With @AnalyzeClasses we tell ArchUnit which Java packages should be analyzed. If you are using JUnit 4 you also need to add the ArchUnit JUnit runner.

Inside the class we create a field and annotate it with @ArchTest. This is our first test.

We can define the constraint we want to validate by using ArchUnits fluent Java API. In this example we want to validate that all classes whose name ends with Service (e.g. UserService) are located in a package named service (e.g. foo.bar.service).

Most ArchUnit rules start with a selector that indicates what type of code units should be validated (classes, methods, fields, etc.). Here, we use the static method classes() to select classes. We restrict the selection to a subset of classes using the that() method (here we only select classes whose name ends with Service). With the should() method we define the constraint that should be matched against the selected classes (here: the classes should reside in a service package).

When running this test class all tests annotated with @ArchTest will be executed. The test will fail, if ArchUnits detects service classes outside a service package.

More examples

Let's look at some more examples.

We can use ArchUnit to make sure that all Logger fields are private, static and final:

// verify that logger fields are private, static and final
@ArchTest
private final ArchRule loggers_should_be_private_static_final = fields()
        .that().haveRawType(Logger.class)
        .should().bePrivate()
        .andShould().beStatic()
        .andShould().beFinal();

Here we select fields of type Logger and define multiple constraints in one rule.

Or we can make sure that methods in utility classes have to be static:

// methods in classes whose name ends with "Util" should be static
@ArchTest
static final ArchRule utility_methods_should_be_static = methods()
        .that().areDeclaredInClassesThat().haveSimpleNameEndingWith("Util")
        .should().beStatic();

To enforce that packages named impl contain no interfaces we can use the following rule:

// verify that interfaces are not located in implementation packages
@ArchTest
static final ArchRule interfaces_should_not_be_placed_in_impl_packages = noClasses()
        .that().resideInAPackage("..impl..")
        .should().beInterfaces();

Note that we use noClasses() instead of classes() to negate the should constraint.

(Personally I think this rule would be much easier to read if we could define the rule as interfaces().should().notResideInAPackage("..impl.."). Unfortunately ArchUnit provides no interfaces() method)

Or maybe we are using the Java Persistence API and want to make sure that EntityManager is only used in repository classes:

@ArchTest
static final ArchRule only_repositories_should_use_entityManager = noClasses()
        .that().resideOutsideOfPackage("..repository")
        .should().dependOnClassesThat().areAssignableTo(EntityManager.class);

Layered architecture example

ArchUnit also comes with some utilities to validate specific architecture styles.

For example can we use layeredArchitecture() to validate access rules for layers in a layered architecture:

@ArchTest
static final ArchRule layer_dependencies_are_respected = layeredArchitecture()
        .layer("Controllers").definedBy("com.mscharhag.archunit.layers.controller..")
        .layer("Services").definedBy("com.mscharhag.archunit.layers.service..")
        .layer("Repositories").definedBy("com.mscharhag.archunit.layers.repository..")
        .whereLayer("Controllers").mayNotBeAccessedByAnyLayer()
        .whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers")
        .whereLayer("Repositories").mayOnlyBeAccessedByLayers("Services");

Here we define three layers: Controllers, Services and Repositories. The repository layer may only accessed by the service layer while the service layer may only be accessed by controllers.

Shortcuts for common rules

To avoid that we have to define all rules our self, ArchUnit comes with a set of common rules defined as static constants. If these rules fit our needs, we can simply assign them to @ArchTest fields in our test.

For example we can use the predefined NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS rule if we make sure no exceptions of type Exception and RuntimeException are thrown:

@ArchTest
private final ArchRule no_generic_exceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;

Summary

ArchUnit is a powerful tool to validate a code base against a set of self defined rules. Some of the examples we have seen are also reported by common static code analysis tools like FindBugs or SonarQube. However, these tools are typically harder to extend with your own project specific rules and this is where ArchUnit comes in.

As always you can find the Sources from the examples on GitHub. If you are interested in ArchUnit you should also check the comprehensive user guide.

Leave a reply