ArchUnitを使用したコード構造の推奨事項の実装

ソフトウェアを構築するとき、私たち全員がチームとして、一般的にベストプラクティスと見なされる一連のガイドラインに従うことに同意します。しかし、開発中に、開発者はこれらのルールを無意識にまたは不本意に破ることができます。私たちは通常、コードレビュー またはSonarQube  、  PMD などのコード品質検査ツールに 依存して いますそのような違反をチェックします。ただし、推奨事項の一部は、SonarQube、PMDなどでは自動化できないソリューションである可能性があります。

たとえば、私は通常、Javaベースのアプリケーションについて以下のガイドラインに従いたいと思っています。

  1. 3層構造 (Web、サービス、リポジトリ層)に従います。この構造では、どの層もすぐ下の層とのみ対話でき、下位層は上位層と対話しないでください。それら。Web層はサービス層と対話でき、サービス層はリポジトリ層と対話できます。ただし、リポジトリ層はサービスまたはWeb層と通信できず、サービス層はWeb層と対話できません。

  2. アプリケーションが大きい場合は、機能ごとパッケージ構造に従うことをお勧めします。この構造は、Webコンポーネントとサービスコンポーネントのみがパブリックであり、残りのコンポーネントはパッケージプライベートである必要があります。

  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ユーザーガイドを読んで、  ArchUnitで できる素晴らしいことを見つけて ください




All Articles