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:
- 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.
- 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:
- Using pagination for collection resources
- 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.