Featured image for "Testing your Java code structure with ArchUnit"

Testing your Java code structure with ArchUnit

November 16th, 2023
8 minute read
ArchUnit JUnit Testing

Intro

When you’re developing an application, you usually try to follow certain conventions. Examples of these conventions are:

To enforce these conventions, a common way to do this is by relying on code reviews. While code reviews are great, they happen very late in the development process. By the time someone reviews your code and notices the irregularities, a developer already wrote all their code, their tests and so on.

Another alternative is that you can automate these tests by using ArchUnit. According to the website, ArchUnit can be defined as:

ArchUnit is a free, simple and extensible library for checking the architecture of your Java code using any plain Java unit test framework.

This means that you can use ArchUnit within a testing framework like JUnit to check whether your code follows certain rules.

Getting started

To start, you first have to add the right dependency depending on the testing framework you use. If you use JUnit 5, you can add the following dependency:

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

After that, you need to create a test. For example:

public class NamingConventionTest {
    @Test
    void myTest() {
        // TODO: Implement
    }
}

Once you created your test class, you have to tell ArchUnit which classes you want to analyze. This is because ArchUnit cannot tell the difference between your classes and the classes you import from libraries.

You can do this by writing the following:

public class NamingConventionTest {
    @Test
    void myTest() {
        // Add this:
        JavaClasses classes = new ClassFileImporter().importPackages("codes.dimitri.myproject");
        // TODO: Implement
    }
}

In this example, codes.dimitri.myproject is the name of the rootpackage of my project.

The next step is to define your architectural rule by using the fluent API provided by ArchUnit. For example, if you want to verify that all your controllers have a name ending with ...Controller, you can write something like:

public class NamingConventionTest {
    @Test
    void controllersShouldHaveNameEndingWithController() {
        JavaClasses classes = new ClassFileImporter().importPackages("codes.dimitri.my-project");
        // Add this:
        ArchRule rule = classes()
            .that().areMetaAnnotatedWith(Controller.class)
            .should().haveSimpleNameEndingWith("Controller");
        // TODO: Implement
    }
}

Then the final step before running your test is to test the ArchRule against the packages you imported by using:

public class NamingConventionTest {
    @Test
    void controllersShouldHaveNameEndingWithController() {
        JavaClasses classes = new ClassFileImporter().importPackages("codes.dimitri.my-project");
        ArchRule rule = classes()
            .that().areMetaAnnotatedWith(Controller.class)
            .should().haveSimpleNameEndingWith("Controller");
        rule.check(classes); // <!-- Add this
    }
}

However, as you can see this would lead to a lot of repetition. To avoid this, you can use the @AnalyzeClasses annotation in stead of the ClassFileImporter. For example:

// Add this:
@AnalyzeClasses(packages = "codes.dimitri.my-project")
public class NamingConventionTest {
    // ...
}

In addition, rather than writing complete test methods, you can define the ArchRule as a field in your test class and annotate it with @ArchTest. For example:

@AnalyzeClasses(packages = "codes.dimitri.my-project")
public class NamingConventionTest {
    @ArchTest
    public static final ArchRule controllersShouldHaveNameEndingWithController = classes()
        .that().areMetaAnnotatedWith(Controller.class)
        .should().haveSimpleNameEndingWith("Controller"); 
}

Exploring the standard API

Checking whether controllers follow a naming convention

As we’ve seen so far, we can test whether a class has a specific suffix by using the haveSimpleNameEndingWith() method. To select the proper classes, we can use the areMetaAnnotatedWith() method to check for classes that are annotated with @Controller. The “meta”-part means that it also includes annotations that are on their own annotated with the given annotation. For example, the @RestController annotation of Spring is also annotated with @Controller. Because of that, classes that are annotated with @RestController are also subject to this rule.

@ArchTest
public static final ArchRule controllersShouldHaveNameEndingWithController = classes()
    .that().areMetaAnnotatedWith(Controller.class)
    .should().haveSimpleNameEndingWith("Controller");

Checking whether certain classes are located in a specific package

In some cases, you want to group certain type of classes together. For example, in some cases you want to group DTOs together in a single package, entities within a different package and so on.

A way to test this is by using:

@ArchTest
public static final ArchRule dtosShouldBeInDTOPackage = classes()
    .that().haveSimpleNameEndingWith("DTO")
    .should().resideInAPackage("..dto..")
    .because("DTOs should be grouped together in a DTO-specific package");

The resideInAnyPackage() method allows you to use a similar syntax as within AOP pointcuts where the double dot matches any number of packages. For example, the ...dto.. pattern would match something like x.y.z.dto or x.dto.foo.

Also interesting is that you can provide a reason within the because() method at the end of your ArchRule. This reason is visible when the test fails, and allows developers to see why a rule was introduced.

Checking whether DTOs are records

Starting with Java 14, records were introduced. Records are immutable data classes, and because of that, they’re ideal for cases like DTOs. To verify that all DTOs are records, you could write a rule like this:

@ArchTest
public static final ArchRule dtosShouldBeRecords = classes()
    .that().haveSimpleNameEndingWith("DTO")
    .should().beRecords()
    .because("DTOs should be immutable");

Checking whether services are interfaces

In some projects, people want to create an interface for each service to reduce coupling. A way to verify this is by verifying that all classes that end with ...Service should be an interface:

@ArchTest
public static final ArchRule servicesShouldBeInterfaces = classes()
    .that().haveSimpleNameEndingWith("Service")
    .should().beInterfaces()
    .because("service contracts should be public interfaces and implementations should be hidden");

Check whether your methods have a specific annotation

If you’re writing a controller, you probably want to make sure that methods that start with get... or find... are annotated with @GetMapping rather than another type of mapping (@PostMapping, …). To enforce this, you could write a rule like this:

@ArchTest
public static final ArchRule findControllerMethodsShouldBeGetMappings = methods()
    .that().arePublic()
    .and().areDeclaredInClassesThat().areMetaAnnotatedWith(Controller.class)
    .and().haveNameStartingWith("find")
    .should().beMetaAnnotatedWith(GetMapping.class);

Similarly, you probably also want to check that all public methods within a controller are either mapping methods or exception handlers. To verify this, you could write a test like this:

@ArchTest
public static final ArchRule publicControllerMethodsShouldBeRequestMappingsOrExceptionHandlers = methods()
    .that().arePublic()
    .and().areDeclaredInClassesThat().areMetaAnnotatedWith(Controller.class)
    .should().beMetaAnnotatedWith(RequestMapping.class)
    .orShould().beMetaAnnotatedWith(ExceptionHandler.class);

Making sure classes don’t rely on other specific classes

If you’re working with Data Transfer Objects or DTOs in your project, you probably want to avoid that a controller can return an entity. To verify this, you could use the noClasses() method to write an inverse rule:

@ArchTest
public static final ArchRule controllersShouldNotRelyOnEntities = noClasses()
    .that().areMetaAnnotatedWith(Controller.class)
    .should().dependOnClassesThat().areMetaAnnotatedWith(Entity.class)
    .because("controllers should use DTOs in stead of entities");

Writing your own conditions

While the ArchUnit API is very extensive, it does not contain a fluent method for all cases. However, this isn’t really a problem as ArchUnit is extensible.

One way to extend ArchUnit is by writing your own conditions. For example, in one of our projects we require that all controller methods are documented with the OpenAPI specification (@Operation) and should contain a security schema. To verify this, we wrote our own ArchCondition:

private static final ArchCondition<JavaMethod> beSecured = new ArchCondition<>("requires security documentation") {
    @Override
    public void check(JavaMethod method, ConditionEvents events) {
        // ...
    }
};

The first step is that we wanted to check whether the given method is annotated with @Operation. To do this, we added the following condition:

if (!method.isAnnotatedWith(Operation.class)) {
    String message = String.format("Method %s is not annotated with @Operation", method.getFullName());
    events.add(SimpleConditionEvent.violated(method, message));
}

In case the method does not follow our condition, we can add a new ConditionEvent. The easiest way to do this is by using the SimpleConditionEvent.violated() method.

The final part is that we wanted to obtain the @Operation annotation itself and check whether it has a security attribute:

Operation operation = method.getAnnotationOfType(Operation.class);
if (operation.security() == null || operation.security().length == 0) {
    String message = String.format(
        "Method %s is annotated with @Operation, but does not contain a security attribute",
        method.getFullName());
    events.add(SimpleConditionEvent.violated(method, message));
}

Now that we have our condition, we can apply it within any ArchRule like this:

@ArchTest
public static final ArchRule controllersShouldHaveOpenAPISpecification = methods()
    .that().areMetaAnnotatedWith(RequestMapping.class)
    .should(beSecured) // This refers to the condition
    .because("controller methods should be documented with Open API");

Generic rules

In addition to defining our own rules, ArchUnit also provides a few rules out of the box. For example, there’s a NO_CLASSES_SHOULD_USE_FIELD_INJECTION rule that you can use to check if you’re using annotations such as @Autowired on your fields.

You can use these standard rules like this:

@ArchTest
public static final ArchRule noClassesShouldUseFieldInjection = GeneralCodingRules.NO_CLASSES_SHOULD_USE_FIELD_INJECTION; 

This also means that you can wrap your own rules into a separate library and include it in multiple projects.

Conclusion

Why should you use ArchUnit

Recently I answered a question on Stack Overflow where the author wanted each method to be annotated with @PreAuthorize. One way of doing this is by adding a runtime check using reflection (an example). The drawbacks of using a runtime check like this is that:

Alternatively, I suggested to write a test with ArchUnit, which has the benefits that:

There is however one thing to realize and that is that, like any other Java framework, ArchUnit is affected by type erasure. This means that it’s not possible to check whether a field is of type List<String> for example and that you can only check for List.class. ArchUnit makes this pretty clear though, because the methods will be named like haveRawType(), doNotHaveRawType(), … .

Another thing I noticed is that IDE’s like IntelliJ do not provide support for @ArchTest. They mark these fields as unused, and do not allow you to run individual tests either.