Test Unitarios de Arquitectura Java (ArchUnit) 

ArchUnit es una biblioteca mediante la cual se puede verificar la arquitectura de nuestro código en Java. Con esta librería podremos verificar distintos aspectos como por ejemplo: verificar las dependencias entre paquetes y clases, verificación de etiquetas, verificación de nomenclatura, etc. 

Para usar la librería tenemos dos opciones:

  • Hacer uso del Initializr para crear nuestro proyecto: lo que añade por defecto la dependencia en el pom.xml. Es la opción más rápida e ideal.
  • En caso de que tengamos un servicio ya creado: y por lo tanto no podamos hacer uso del Initialzr, deberemos añadir la dependencia al archivo pom manualmente. 

Suponiendo que estamos en el segundo caso, tendremos que generar la dependencia añadiendo el siguiente código en el archivo pom.xml de nuestro proyecto:

<!-- https://mvnrepository.com/artifact/com.tngtech.archunit/archunit-junit5-api --> 
<dependency> 
       <groupId>com.tngtech.archunit</groupId> 
       <artifactId>archunit-junit5</artifactId> 
       <version>0.14.1</version> 
       <scope>test</scope> 
</dependency> 
<dependency> 
       <groupId>org.springframework.boot</groupId> 
       <artifactId>spring-boot-starter-test</artifactId> 
       <scope>test</scope> 
       <exclusions> 
             <exclusion> 
                       <groupId>org.junit.vintage</groupId> 
                       <artifactId>junit-vintage-engine</artifactId> 
             </exclusion> 
       </exclusions> 
</dependency> 
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> 
<dependency> 
       <groupId>org.junit.jupiter</groupId> 
       <artifactId>junit-jupiter-api</artifactId> 
       <version>5.7.0</version> 
       <scope>test</scope> 
</dependency> 
 
<plugin> 
      <groupId>org.apache.maven.plugins</groupId> 
      <artifactId>maven-surefire-plugin</artifactId> 
      <version>2.22.2</version> 
</plugin> 

Por otro lado, se deberá crear un fichero dentro de la carpeta «resources» de los tests que se llame «archunit.properties» , y dentro del mismo colocar la siguiente la siguiente línea de código: 

resolveMissingDependenciesFromClassPath=false 

Ejemplos de uso

En caso de hacer uso del Initializr, se incluyen por defecto cuatro test los cuales comprueban que la implementación de controladores, servicios y DTOs sigan el estándar de arquitectura. Veamos algunos ejemplos: 

Ejemplo comprobación de anotaciones de controlador y servicio

public class AnnotationsTest { 

    private JavaClasses javaClasses; 
 
    @BeforeEach 
    public void init() { 

        this.javaClasses = new ClassFileImporter().importPackages("com.minsait.onesait.pruebaarchunit"); 
    } 

    @Test 
    public void givenControllers_thenClassesAreAnnotatedWithRestController() {     

        ArchRule controllerRule = classes().that().resideInAPackage("..controller..").should() 
        .beAnnotatedWith(RestController.class); 

        controllerRule.check(this.javaClasses); 
    }

    @Test 
    public void givenServices_thenClassesAreAnnotatedWithService() { 

        ArchRule serviceRule = classes().that().resideInAPackage("..services.impl").should() 
        .beAnnotatedWith(Service.class);
 
        serviceRule.check(this.javaClasses); 
    } 

} 

Ejemplo de nomenclatura, acceso a los campos y comprobación de interfaces

public class ClassesTest { 

    private JavaClasses javaClasses; 

    @BeforeEach 
    public void init() { 

        this.javaClasses = new ClassFileImporter().importPackages("com.minsait.onesait.pruebaarchunit"); 
    } 
 
    @Test 
    public void givenClasses_thenAreContainedInCorrectPackages() { 

        ArchRule rule1 = classes() 
                                .that().haveSimpleNameEndingWith("Controller") 
                                .should().resideInAPackage("..controllers"); 

        ArchRule rule2 = classes() 
                                .that().haveSimpleNameEndingWith("Service") 
                                .should().resideInAPackage("..services"); 

        ArchRule rule3 = classes() 
                                .that().resideInAPackage("..model") 
                                .and().areNotNestedClasses() 
                                .should().haveSimpleNameEndingWith("DTO"); 

        rule1.check(this.javaClasses); 
        rule2.check(this.javaClasses); 
        rule3.check(this.javaClasses); 
    } 


    @Test 
    public void givenModelDTOAndEntity_thenFieldsArePrivate() { 

        this.javaClasses = new ClassFileImporter().importPackages("com.minsait.onesait.pruebaarchunit.model"); 

        ArchRule dtoRule = fields() 
                                .should().bePrivate(); 

        dtoRule.check(this.javaClasses); 
    } 

    @Test 
    public void givenService_thenMustBeInterfaces() { 

        ArchRule rule1 = classes() 
                                .that().resideInAPackage("..services") 
                                .should().beInterfaces(); 

         rule1.check(this.javaClasses); 
    }   
} 

Comprobación de la no inserción de interfaces en los paquetes de implementación

@AnalyzeClasses(packages = "com.minsait.onesait.pruebaarchunit") 
public class CodingRulesTest { 

@ArchTest 
private final ArchRule no_generic_exceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS; 


@ArchTest 
private final ArchRule interfaces_must_not_be_placed_in_implementation_packages = noClasses().that() 

.resideInAPackage("..impl..").should().beInterfaces(); 

} 

Comprobación de dependencia entre clases y acceso entre las mismas

public class LayerTest { 

    private JavaClasses javaClasses; 


    @BeforeEach 
    public void init() { 
        this.javaClasses = new ClassFileImporter().importPackages("com.minsait.onesait.pruebaarchunit"); 
    } 

    @Test 
    public void givenControllerLayer_thenDependsOnServiceLayer() { 

        ArchRule controllerRule = classes() 
                                        .that() 
                                        .resideInAPackage("..controller..") 
                                        .should().dependOnClassesThat() 
                                        .resideInAPackage("..service.."); 

        controllerRule.check(this.javaClasses); 
    } 

    @Test 
    public void givenALayerArchitecture_thenNoLayerViolationShouldExist() { 

        LayeredArchitecture architecture = layeredArchitecture() 
                                                            .layer("controller").definedBy("..controllers..") 
                                                            .layer("service").definedBy("..services.impl") 
                                                            .whereLayer("controller").mayNotBeAccessedByAnyLayer() 
                                                            .whereLayer("service").mayOnlyBeAccessedByLayers("controller"); 
                                              
        architecture.check(this.javaClasses); 
    }     
} 

Referencias: para más información, se puede consultar la documentación oficial de ArchUnit.

Cabecera: foto de Shapelined en Unsplash

Autor

2 comentarios en «Test Unitarios de Arquitectura Java (ArchUnit) »

  • el 05/07/2022 a las 18:05
    Enlace permanente

    Hola Rodrigo,

    Gracias por el artículo. Nosotros en TA lo venimos utilizando desde hace tiempo en nuestros proyectos.

    Tienes un typo: el nombre del archivo es «archunit.properties» (no achunit.properties).

    Saludos,
    Paco A

    Respuesta
  • el 06/07/2022 a las 15:24
    Enlace permanente

    Buenas tardes, Paco.

    Gracias por tu comentario y por avisarnos de la errata; ya la hemos corregido.

    Un saludo.

    Respuesta

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *