Implementación de recomendaciones de estructura de código usando ArchUnit

Al crear software, todos, como equipo, acordamos seguir un conjunto de pautas que generalmente se consideran las mejores prácticas. Sin embargo, durante el desarrollo, los desarrolladores pueden romper estas reglas sin saberlo o sin quererlo. Por lo general, confiamos en  revisiones de código  o verificadores de calidad de código como  SonarQube  ,  PMD,  etc. para verificar tales violaciones. Pero algunas de las recomendaciones pueden ser soluciones que no se pueden automatizar con SonarQube, PMD, etc.

Por ejemplo, normalmente quiero seguir las siguientes pautas para mis aplicaciones basadas en Java:

  1. Siga una estructura de tres niveles  (web, servicio, niveles de repositorio) donde cualquier nivel solo puede interactuar con el nivel inferior inmediato, y el nivel inferior no debe interactuar con el nivel superior. aquellos. el nivel web puede interactuar con el nivel de servicio, el nivel de servicio puede interactuar con el nivel de repositorio. Pero el nivel de repositorio no puede comunicarse con el servicio o el nivel web, el nivel de servicio no puede interactuar con el nivel web.

  2. Si la aplicación es grande, es posible que deseemos seguir la estructura Paquete por característica, donde solo los componentes Web y Servicio son públicos y el resto de los componentes deben ser paquetes privados.

  3. Cuando use Spring Dependency Injection, no use la inyección basada en campo y prefiera la inyección basada en constructor.

Por tanto, puede haber muchas reglas que queramos seguir. La buena noticia es que podemos verificar la implementación de estas recomendaciones usando pruebas JUnit usando ArchUnit .

Aquí guía User ArchUnit .

Veamos cómo podemos usar ArchUnit para probar nuestras pautas de arquitectura.

Agregue la siguiente dependencia archunit-junit5 .

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

Echemos un vistazo a cómo podemos aplicar las diversas pautas que mencioné anteriormente.

Regla 1. Los servicios y repositorios no deben interactuar con el nivel 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);
}

, .

Lea la Guía de usuario oficial de  ArchUnit  y descubra las cosas increíbles que puede hacer con  ArchUnit .




All Articles