Implémentation des recommandations de structure de code à l'aide d'ArchUnit

Lors de la création de logiciels, nous acceptons tous, en tant qu'équipe, de suivre un ensemble de directives qui sont généralement considérées comme des meilleures pratiques. Cependant, pendant le développement, les développeurs peuvent, sans le savoir ou non, enfreindre ces règles. Nous nous appuyons généralement sur  des revues de code  ou des vérificateurs de qualité de code comme  SonarQube  ,  PMD,  etc. pour vérifier de telles violations. Mais certaines des recommandations peuvent être des solutions qui ne peuvent pas être automatisées avec SonarQube, PMD, etc.

Par exemple, je souhaite généralement suivre les instructions ci-dessous pour mes applications Java:

  1. Suivez une structure à trois niveaux  ( niveaux Web, service, référentiel) où tout niveau ne peut interagir qu'avec le niveau inférieur immédiat, et le niveau inférieur ne doit pas interagir avec le niveau supérieur. ceux. le niveau Web peut interagir avec le niveau de service, le niveau de service peut interagir avec le niveau de référentiel. Mais le niveau de référentiel ne peut pas communiquer avec le service ou le niveau Web, le niveau de service ne peut pas interagir avec le niveau Web.

  2. Si l'application est volumineuse, nous souhaitons peut-être suivre la structure Package-By-Feature, où seuls les composants Web et Service sont publics et le reste des composants doit être privé du package.

  3. Lorsque vous utilisez Spring Dependency Injection, n'utilisez pas l' injection basée sur le champ et préférez l'injection basée sur le constructeur.

Ainsi, il peut y avoir de nombreuses règles que nous voulons suivre. La bonne nouvelle est que nous pouvons vérifier la mise en œuvre de ces recommandations à l'aide de tests JUnit à l'aide d' ArchUnit .

Voici le guide utilisateur ArchUnit .

Voyons comment nous pouvons utiliser ArchUnit pour tester nos directives d'architecture.

Ajoutez la dépendance archunit-junit5 suivante .

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

Voyons comment nous pouvons appliquer les différentes directives que j'ai mentionnées ci-dessus.

Règle 1. Les services et référentiels ne doivent pas interagir avec le niveau 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);
}

, .

Veuillez lire le UserGuide officiel d'  ArchUnit  et découvrez ce que vous pouvez faire avec  ArchUnit .




All Articles