Posted by Thiago Chaves

ArchUnit - Checking architectural rules in Java code

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));
        }
      }
    };
  }
}
Tagged ,

Testing Without Mocks

As seen in James Shore's page, the idea is to write tests that do not depend on mocks.

Tagged ,

Tips for Architecture

When a system that needs to be fast has to interact with a slow system:

  • see if you can make the slow system depend on the fast system instead;
  • see if you can cache the slow system's results;
  • see if you can make the slow system asynchronous;
  • see if you can make the slow system parallel;
Tagged ,