This is merely a reminder for me of my journey learning ArchUnit, a library for checking architectural rules in Java code. It allows you to define and enforce rules about the structure of your codebase, such as package dependencies, class relationships, and more.
Configuring ArchUnit in Your Project
First, you need to add ArchUnit to your project dependencies. In my case, using Gradle and JUnit 5, I added the following to my build.gradle file:
testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.1'
After testing one time, I did not like how vague the error message was on the console after a gng test1. So I updated the test task in build.gradle as follows:
testLogging {
events "failed"
exceptionFormat "full"
showExceptions true
showCauses true
showStackTraces true
}
Then I created a new test class. I put it in the module that was the only one with integration tests. I have to check if I could place it in a centralized way so that all modules could be tested against the same rules. The class is this:
package com.company.project.architecture;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import com.tngtech.archunit.core.domain.JavaAnnotation;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import java.util.List;
import org.junit.jupiter.api.Tag;
import org.springframework.boot.test.context.SpringBootTest;
/**
* Checks that all test classes with @SpringBootTest annotation are also annotated with
* @Tag("integration").
*/
@AnalyzeClasses(packages = "com.company.project")
public class CheckSpringBootTests {
@ArchTest
static final ArchRule SPRING_BOOT_TESTS_MUST_BE_TAGGED =
classes()
.that()
.areAnnotatedWith(SpringBootTest.class)
.should()
.beAnnotatedWith(Tag.class)
.andShould(haveTagWithValue("integration"))
.because("@SpringBootTest tests require @Tag(\"integration\")");
private static ArchCondition<JavaClass> haveTagWithValue(String expectedValue) {
return new ArchCondition<JavaClass>("have @Tag with value \"" + expectedValue + "\"") {
@Override
public void check(JavaClass item, ConditionEvents events) {
List<JavaAnnotation<JavaClass>> tagAnnotations =
item.getAnnotations().stream()
.filter(annotation -> annotation.getRawType().isEquivalentTo(Tag.class))
.toList();
boolean found = false;
for (JavaAnnotation<JavaClass> tagAnnotation : tagAnnotations) {
Object value = tagAnnotation.getProperties().get("value");
if (value instanceof String && expectedValue.equals(value)) {
found = true;
break;
}
}
if (!found) {
String message =
String.format(
"Class '%s' is annotated with @SpringBootTest but does not have @Tag with value '%s'",
item.getName(), expectedValue);
events.add(SimpleConditionEvent.violated(item, message));
}
}
};
}
}