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
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
Buenas tardes, Paco.
Gracias por tu comentario y por avisarnos de la errata; ya la hemos corregido.
Un saludo.