Codifying Your Technical / Architectural Standards with ArchUnit

Featured image for sharing metadata for article

Whether you're in a team that's still meshing with your technical skills, or you've been working as a tight-knit group for years, there are likely going to be cases where someone trips up in terms of following architectural patterns in your codebase.

In my previous team, we built up a "technical ways of working" document that constantly evolved as we discussed changes at code review, then documented the decision for the future.

The problem with this approach is that the result was a document stored outside of our codebase, which meant it was less easy to follow and to add as part of the review process. Although we used tools like Spotless to automate code style, we had no guardrails for architecture of the internal codebase.

This led to times where previous code review time was dedicated to commenting on someone not following the agreed patterns of the team, instead of focussing on the important things - like the feature we were trying to deliver.

About 18 months ago, one of my colleagues at the time, Lewis, did a talk about ArchUnit and started to flesh out the rules that were available for common libraries we used, but I'd not gotten around to looking at it in any depth. We'd recently started to add them into Wiremock.

After learning about jMolecules at yesterday's jChampions, I've started to properly dig into ArchUnit, and will have more on jMolecules in a later post πŸ‘€

ArchUnit is a great test framework for building unit (integration?) tests around our code style and structure, and it's a really powerful addition to any Java codebase, which can help codify our standards into tests, so we don't need to worry about them when it comes to code review.

Code snippets can be found in full in a sample project on GitLab.

Example: Don't allow @Autowired on fields

For instance, let's say that we're using a Spring (Boot) application, and we want to make sure that we don't write classes using field injection:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class BadBean {
    @Autowired
    private Object field;
}

Although we may see warnings in our IDE/in tooling such as SonarQube, there's nothing actively stopping someone doing this, unless it is caught by a human in i.e. code review.

We'd be able to write the following ArchUnit test:

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.springframework.beans.factory.annotation.Autowired;

import static com.tngtech.archunit.base.DescribedPredicate.describe;
import static com.tngtech.archunit.base.DescribedPredicate.equalTo;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

@AnalyzeClasses(packages = "me.jvt.hacking")
class ArchitectureTest {
  @ArchTest
    ArchRule fieldsShouldNotBeAutowired =
    classes()
      .that()
      .haveNameNotMatching(".*Test")
      .and()
      .areNotAnnotatedWith(Nested.class)
      .and()
      .containAnyFieldsThat(
          describe(
            "are Autowired by Spring",
            f -> f.tryGetAnnotationOfType(Autowired.class).isPresent()))
      .should()
      .containNumberOfElements(equalTo(0));
}

When run on the above, we can see the following test failures:

Architecture Violation [Priority: MEDIUM] - Rule 'classes that have name not matching '.*Test' and are top level classes and contain any fields that are Autowired by Spring should contain number of elements equal to '0'' was violated (1 times):
there is/are 1 element(s) in classes [me.jvt.hacking.BadBean]
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that have name not matching '.*Test' and are top level classes and contain any fields that are Autowired by Spring should contain number of elements equal to '0'' was violated (1 times):
there is/are 1 element(s) in classes [me.jvt.hacking.BadBean]

Example: Require naming of mocked fields

Now, let's say that we want to replace a class like so:

@ExtendWith(MockitoExtension.class)
class AddMockPrefixToMockedFieldsTest {
  @Mock private BadBean bean;

  @Test
  void isNotNull() {
    // not a great test, but checks that Mockito is working
    assertThat(bean).isNotNull();
  }
}

We should have a class like, using @Mock and the MockitoExtension to manage mock lifecycles:

@ExtendWith(MockitoExtension.class)
class AddMockPrefixToMockedFieldsTest {
  @Mock private BadBean bean;

  @Test
  void isNotNull() {
    // not a great test, but checks that Mockito is working
    assertThat(bean).isNotNull();
  }
}

This can be represented with an ArchUnit rule:

@ArchTest
ArchRule mockAnnotationRequiresMockPrefix =
  fields()
    .that()
    .areDeclaredInClassesThat()
    .resideOutsideOfPackage("..architecture") // avoid this test class
    .and()
    .areDeclaredInClassesThat()
    .haveNameMatching(".*Test")
    .or()
    .areDeclaredInClassesThat()
    .areAnnotatedWith(Nested.class)
    .and()
    .areAnnotatedWith(Mock.class)
    .should()
    .haveNameMatching("mock.*");

Which displays the following error when executed against the problematic code:

Architecture Violation [Priority: MEDIUM] - Rule 'fields that are declared in classes that reside outside of package '..architecture' and are declared in classes that have name matching '.*Test' or are declared in classes that are annotated with @Nested and are annotated with @Mock should have name matching 'mock.*'' was violated (1 times):
Field <me.jvt.hacking.examples.AddMockPrefixToMockedFieldsTest.bean> does not match 'mock.*' in (AddMockPrefixToMockedFieldsTest.java:0)
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'fields that are declared in classes that reside outside of package '..architecture' and are declared in classes that have name matching '.*Test' or are declared in classes that are annotated with @Nested and are annotated with @Mock should have name matching 'mock.*'' was violated (1 times):
Field <me.jvt.hacking.examples.AddMockPrefixToMockedFieldsTest.bean> does not match 'mock.*' in (AddMockPrefixToMockedFieldsTest.java:0)

Example: Require use of @Mock on mock fields

On the other hand, we may already have a lot of fields that are currently instantiated with mock:

class AddMockToFieldsTest {
    private List<BadBean> mockBeans;

    @BeforeEach
    void setup() {
        mockBeans = mock(List.class); // Unchecked assignment: 'java.util.List' to 'java.util.List<me.jvt.hacking.BadBean>'
    }

    @Test
    void isNotNull() {
        // not a great test, but checks that Mockito is working
        assertThat(mockBeans).isNotNull();
    }
}

But we want to migrate them to:

@ExtendWith(MockitoExtension.class)
class AddMockToFieldsTest {
    @Mock
    private List<BadBean> mockBeans;

    @Test
    void isNotNull() {
        // not a great test, but checks that Mockito is working
        assertThat(mockBeans).isNotNull();
    }
}

To do this, we'd create the following ArchUnit test:

@ArchTest
ArchRule mockPrefixRequiresMockAnnotation =
  fields()
    .that()
    .areDeclaredInClassesThat()
    .resideOutsideOfPackage("..architecture") // avoid this test class
    .and()
    .areDeclaredInClassesThat()
    .haveNameMatching(".*Test")
    .or()
    .areDeclaredInClassesThat()
    .areAnnotatedWith(Nested.class)
    .and()
    .haveNameMatching("mock.*")
    .should()
    .beAnnotatedWith(Mock.class);

And would get a handy error message:

Architecture Violation [Priority: MEDIUM] - Rule 'fields that are declared in classes that reside outside of package '..architecture' and are declared in classes that have name matching '.*Test' or are declared in classes that are annotated with @Nested and have name matching 'mock.*' should be annotated with @Mock' was violated (1 times):
Field <me.jvt.hacking.examples.AddMockToFieldsTest.mockBeans> is not annotated with @Mock in (AddMockToFieldsTest.java:0)
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'fields that are declared in classes that reside outside of package '..architecture' and are declared in classes that have name matching '.*Test' or are declared in classes that are annotated with @Nested and have name matching 'mock.*' should be annotated with @Mock' was violated (1 times):
Field <me.jvt.hacking.examples.AddMockToFieldsTest.mockBeans> is not annotated with @Mock in (AddMockToFieldsTest.java:0)

Example: Require final fields

Something quite commonly required is to make sure that we use immutable classes, the first step of which is to make sure that we can't create a class like so, with mutable fields:

public class NonFinalFields {
  private int count;
  private String version;

  public NonFinalFields(int count, String version) {

    this.count = count;
    this.version = version;
  }

  public int getCount() {
    return count;
  }

  public String getVersion() {
    return version;
  }
}

So we'd create the following ArchUnit rule to enforce this:

@ArchTest
ArchRule alwaysFinalFields =
  classes()
    .that()
    .haveNameNotMatching(".*Test")
    .and()
    .areNotAnnotatedWith(Nested.class)
    .and()
    .haveNameNotMatching(".*Dao")
    .and()
    .resideOutsideOfPackage("..architecture")
    .should()
    .haveOnlyFinalFields();

And then we get the following error:

Architecture Violation [Priority: MEDIUM] - Rule 'classes that have name not matching '.*Test' and are not annotated with @Nested and have name not matching '.*Dao' and reside outside of package '..architecture' should have only final fields' was violated (2 times):
Field <me.jvt.hacking.NonFinalFields.count> is not final in (NonFinalFields.java:0)
Field <me.jvt.hacking.NonFinalFields.version> is not final in (NonFinalFields.java:0)
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that have name not matching '.*Test' and are not annotated with @Nested and have name not matching '.*Dao' and reside outside of package '..architecture' should have only final fields' was violated (2 times):
Field <me.jvt.hacking.NonFinalFields.count> is not final in (NonFinalFields.java:0)
Field <me.jvt.hacking.NonFinalFields.version> is not final in (NonFinalFields.java:0)

Conclusion

Hopefully this gives you a bit of a flavour of what you can do with ArchUnit, and what you may want to think about for your projects to maintain consistent codebases.

You may also fancy a look at the example use cases ArchUnit documents.

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #java #archunit #code-review.

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.