Implementing Code Structure Recommendations Using ArchUnit

When building software, we all, as a team, agree to follow a set of guidelines that are generally considered best practices. However, during development, developers may unknowingly or unwillingly break these rules. We usually rely on  code reviews  or code quality checkers like  SonarQube  ,  PMD,  etc. to check for such violations. But some of the recommendations might be solutions that cannot be automated with SonarQube, PMD, etc.

For example, I usually want to follow the guidelines below for my Java based applications:

  1. Follow a three-tier structure  (web, service, repository tiers) where any tier can only interact with the immediate lower tier, and the lower tier should not interact with the upper tier. those. the web tier can interact with the service tier, the service tier can interact with the repository tier. But the repository tier cannot communicate with the service or web tier, the service tier cannot interact with the web tier.

  2. If the application is large, we might want to follow the Package-By-Feature structure , where only the Web and Service components are public and the rest of the components must be package-private.

  3. When using Spring Dependency Injection, don't use field-based injection and prefer constructor-based injection.

Thus, there may be many rules that we want to follow. The good news is that we can verify the implementation of these recommendations using JUnit tests using ArchUnit .

Here guide User ArchUnit .

Let's see how we can use ArchUnit to test our architecture guidelines.

Add the following dependency archunit-junit5 .

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

Let's take a look at how we can apply the various guidelines I mentioned above.

Rule 1. Services and repositories should not interact with the web tier.

package com.sivalabs.moviebuffs;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;

class ArchTest {

    @Test
    void servicesAndRepositoriesShouldNotDependOnWebLayer() {
      JavaClasses importedClasses = new ClassFileImporter()
          .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
          .importPackages("com.sivalabs.moviebuffs");

      noClasses()
          .that().resideInAnyPackage("com.sivalabs.moviebuffs.core.service..")
            .or().resideInAnyPackage("com.sivalabs.moviebuffs.core.repository..")
          .should()
            .dependOnClassesThat()
            .resideInAnyPackage("com.sivalabs.moviebuffs.web..")
          .because("Services and repositories should not depend on web layer")
          .check(importedClasses);
    }
}

ArchUnit DSL, , .  , , .

2:

SpringBoot   ,    . , Web Config .

.

@Test
void shouldFollowLayeredArchitecture() {
  JavaClasses importedClasses = new ClassFileImporter()
          .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
          .importPackages("com.sivalabs.moviebuffs");

  layeredArchitecture()
      .layer("Web").definedBy("..web..")
      .layer("Config").definedBy("..config..")
      .layer("Service").definedBy("..service..")
      .layer("Persistence").definedBy("..repository..")

      .whereLayer("Web").mayNotBeAccessedByAnyLayer()
      .whereLayer("Service").mayOnlyBeAccessedByLayers("Config", "Web")
      .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
      .check(importedClasses);
}

3: Spring @Autowired

@Test
void shouldNotUseFieldInjection() {
    JavaClasses importedClasses = new ClassFileImporter()
          .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
          .importPackages("com.sivalabs.moviebuffs");

    noFields()
      .should().beAnnotatedWith(Autowired.class)
      .check(importedClasses);
}

4:

, ,  Service  ..

@Test
void shouldFollowNamingConvention() {
    JavaClasses importedClasses = new ClassFileImporter()
        .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
        .importPackages("com.sivalabs.moviebuffs");
    classes()
        .that().resideInAPackage("com.sivalabs.moviebuffs.core.repository")
        .should().haveSimpleNameEndingWith("Repository")
        .check(importedClasses);

    classes()
        .that().resideInAPackage("com.sivalabs.moviebuffs.core.service")
        .should().haveSimpleNameEndingWith("Service")
        .check(importedClasses);
}

5: JUnit 5

 JUnit 5   .  JUnit 4 (… Testcontainers β€¦ ), / JUnit4 ,   @Test , Assert .. .

JUnit 4 :

@Test
void shouldNotUseJunit4Classes() {
    JavaClasses classes = new ClassFileImporter()
        .importPackages("com.sivalabs.moviebuffs");

    noClasses()
        .should().accessClassesThat().resideInAnyPackage("org.junit")
        .because("Tests should use Junit5 instead of Junit4")
        .check(classes);

    noMethods().should().beAnnotatedWith("org.junit.Test")
        .orShould().beAnnotatedWith("org.junit.Ignore")
        .because("Tests should use Junit5 instead of Junit4")
        .check(classes);
}

, .

Please read the official  ArchUnit UserGuide  and find out what awesome things you can do with  ArchUnit .




All Articles