使用ArchUnit实施代码结构建议

在构建软件时,我们所有人作为一个团队,都同意遵循通常被认为是最佳实践的一组准则。但是,在开发过程中,开发人员可能在不知不觉中或不愿意违反这些规则。我们通常依靠 代码审查 或代码质量检查器(如 SonarQube  ,  PMD 等)。检查是否存在此类违规行为。但是其中一些建议可能是无法使用SonarQube,PMD等自动化的解决方案。

例如,对于基于Java的应用程序,我通常希望遵循以下准则:

  1. 遵循三层结构 (Web,服务,存储库层),其中任何层都只能与直接下层交互,而下层则不应与上层交互。那些。Web层可以与服务层交互,服务层可以与存储库层交互。但是存储库层无法与服务或Web层进行通信,服务层无法与Web层进行交互。

  2. 如果应用程序很大,我们可能希望遵循Package-By-Feature结构其中只有Web和Service组件是公共的,其余组件必须是Package-private的。

  3. 使用Spring Dependency Injection时,不要使用基于字段的注入,而应该使用基于构造函数的注入。

因此,我们可能要遵循许多规则。好消息是,我们可以使用ArchUnit使用JUnit测试来验证这些建议的实现

这里指导用户ArchUnit

让我们看看如何使用ArchUnit来测试我们的架构准则。

添加以下依赖项archunit-junit5

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

让我们看一下如何应用上面提到的各种准则。

规则1.服务和存储库不应与Web层交互。

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

, .

请阅读官方的 ArchUnit UserGuide  ,了解使用ArchUnit可以做些 什么




All Articles