Using ArchUnit To Enforce Architecture Best Practices


I have worked with multiple software development teams that because of feature delivery pressure does not apply best practices. This later leads to tech debt and cost more time. In the last project team that I helped they made two small mistakes:

  1. They didn’t use pagination in the collection resources. They were fetching the list of data from the database and returning back to the user as JSON. During development when data was small they didn’t face any issues. But, later when customer started doing testing it became a monumental task to add pagination in all the collection resources.
  2. They were returning domain entities in the response. This meant they were transferring more data over the wire than it was necessary.

So, they were breaking two best practices:

  1. Using pagination for collection resources
  2. Keep your domain object different from representation object

I was thinking if there is a way I can enforce these two best practices. Today, I stumbled upon an interesting project ArchUnit that allows us to codify these rules as tests. As per the Github project,

ArchUnit is a Java architecture test library, to specify and assert architecture rules in plain Java

In this quick post I will share how I created a couple of tests that can enforce these practices.

Before we start you need to add library to your build file.

Gradle

testImplementation 'com.tngtech.archunit:archunit:0.13.1'

Maven

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit</artifactId>
    <version>0.13.1</version>
    <scope>test</scope>
</dependency>

Now we can write our test.

Let’s start by creating a simple Spring Boot REST API controller.

package com.example.controller;

@RestController
@RequestMapping("/todos")
class TodoController {

    @GetMapping
    public List<Todo> getTodoList() {
        return Collections.emptyList();
    }

}

package com.example.domain;
class Todo {
}

Above is a REST controller that has getTodoList method that return List.

Our first ArchUnit test will ensure that we don’t allow List as valid return type in GetMapping methods.

@Test
public void no_list_set_rule() {
    JavaClasses javaClasses = new ClassFileImporter().importClasses(TodoController.class);

    ArchRule rule = noMethods().that()
            .areAnnotatedWith(GetMapping.class)
            .should()
            .haveRawReturnType(List.class)
            .orShould()
            .haveRawReturnType(Set.class);

    rule.check(javaClasses);
}

The code shown above checks that there should be no method in the TodoController class with GetMapping that has List or Set as the return type.

The above test will fail when you run against our code as the return type in our method is List.

The failure response will be as shown below.

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no methods that are annotated with @GetMapping should have raw return type java.util.List or should have raw return type java.util.Set' was violated (1 times):
Method <com.example.TodoController.getTodoList()> has raw return type java.util.List in (ArchitectureTest.java:0)

To make it pass we should make it return Page.

package com.example.controller;

import org.springframework.data.domain.Page;

@RestController
@RequestMapping("/todos")
class TodoController {

    @GetMapping
    public Page<Todo> getTodoList() {
        return Page.empty();
    }

}

But the above test will pass if instead of Page it was Foo.

We can add another test that can rely on our convention that we want to return Page object in methods whose name end with List.

@Test
public void pagination_rule() {
    JavaClasses javaClasses = new ClassFileImporter().importClasses(TodoController.class);

    ArchRule rule = methods().that()
            .areAnnotatedWith(GetMapping.class)
            .and()
            .haveNameMatching("\\w*List\\b")
            .should()
            .haveRawReturnType(Page.class);
    rule.check(javaClasses);
}

The two test methods give us confidence that developers will implement pagination in collection methods.

Now, let’s move on two over second best practice Keep your domain object different from representation object.

This looks like a simple requirement. We just need to check the return type of our method. Rather than Page it should be Page.

But, as it turns out ArchUnit does not give you a way to assert the generic type like TodoDto. It only has assertion for raw types like List.

It took me a while to figure out a way. After reading many Github issues in the ArchUnit repo I figured out a way.

@Test
@DisplayName("REST controllers collection resources should only return DTOs")
void dto_rule() {
    JavaClasses javaClasses = new ClassFileImporter().importClasses(TodoController.class);
    ArchRule rule = methods()
            .that()
            .areAnnotatedWith(GetMapping.class)
            .and().haveRawReturnType(Page.class)
            .should(new ArchCondition<JavaMethod>("return DTO object") {
                @Override
                public void check(JavaMethod item, ConditionEvents events) {
                    Type genericReturnType = item.reflect().getGenericReturnType();
                    Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
                    for (Type actualTypeArgument : actualTypeArguments) {
                        if (!actualTypeArgument.getTypeName().endsWith("Dto")) {
                            events.add(new SimpleConditionEvent(item, false, "Not returning Page<DTO> object"));
                        }
                    }
                }
            });

    rule.check(javaClasses);
}

The magic is in the should method. It gives you access to the generic return type using the reflect method. Once you have access to the generic Type. We can find all the type parameters and assert them. In the code example should above I am asserting the type name should end with Dto.

If you run the test above it will fail with following error message.

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'methods that are annotated with @GetMapping and have raw return type org.springframework.data.domain.Page should return DTO object' was violated (1 times):
Not returning Page<DTO> object

To fix it we will return Page instead of Page

@RestController
@RequestMapping("/todos")
class TodoController {

    @GetMapping
    public Page<TodoDto> getTodoList() {
        return Page.empty();
    }

}

Conclusion

I have barely touched the surface of the ArchUnit API. It also has rules to enforce layering and onion architecture constraints. I always prefer code way of doing things as it gives you flexibility to meet your needs. I will be using it in future projects to see how it works in the real world. After playing with this library for a couple of hours I have gained enough confidence to use it in my next assignment.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s