¿Cuántas veces os ha sucedido que el código del sistema software que estáis implementando difiere del diseño y arquitectura originalmente planteados? Esta situación puede ser un problema bastante común, especialmente en aquellas aplicaciones de mayores dimensiones.

ArchUnit es una nueva herramienta que surge para ayudar a verificar que una implementación de código es consistente con la arquitectura original definida. Los test de ArchUnit se escriben y ejecutan como un test unitario cualquiera y proporciona al equipo de desarrollo un feedback inmediato sobre el trabajo que están realizando. Además, al incluirlo en el ciclo de integración continua, podremos ver si una build del producto puede ser construida, verificando que cumple con los requisitos arquitectónicos deseados.

En este post veremos las posibilidades ofrecidas por esta librería, en qué basa su funcionamiento y un hands-on para conocer de forma práctica el funcionamiento de la librería.

Propósito

ArchUnit es una librería open source de testing escrita en Java, con la que es posible verificar que una aplicación Java se adhiere a un conjunto de reglas arquitectónicas. Nos podemos referir al término arquitectura como la manera en que se organizan las distintas clases de una aplicación en paquetes, así como estas pueden llegar a comunicarse entre sí, mediante las dependencias existentes entre las clases que contienen.

La arquitectura de software es un aspecto importante en todo desarrollo. ¿Por qué? Algunos de los objetivos de la arquitectura de software, cuando hablamos de código fuente, son mantenibilidad, reemplazabilidad y extensibilidad. Para desarrollar un sistema software que maximice estas 3 características, se necesitará asegurar que dicho sistema sea modular y que las interdependencias sean lo más pequeñas posibles, alcanzando una alta cohesión y un bajo acoplamiento.

Disponer de pruebas automáticas para poder verificar la arquitectura es un aspecto beneficioso tanto para nuevos desarrollos de aplicaciones como para aplicaciones ya existentes. En este último caso, al disponer de pruebas automáticas, se puede hacer evolucionar las reglas definidas en los tests, ver en consecuencia cómo deben cambiar los componentes ya existentes y, por otro lado, asegurar que los nuevos componentes cumplen con las reglas ya definidas. Además de lo expuesto, también ayuda a transmitir a los nuevos desarrolladores las buenas prácticas, a nivel de arquitectura, acordadas en el proyecto.

Funcionamiento y posibilidades

La primera pregunta que se puede plantear cualquier persona que esté leyendo este post es cómo funciona ArchUnit. Pues bien, la librería hace uso del API de reflexión de Java y del análisis del bytecode, utilizando para ello, la herramienta ASM. Toda aquella información que la librería no puede obtener por medio del API de reflexión es obtenida por medio del análisis del bytecode, como puede ser la dependencia entre dos clases de un programa.

La librería de ArchUnit se divide en diferentes capas, donde las más importantes son la capa Core, la capa Lang y la capa Library. En pocas palabras, el Core se ocupa de la infraestructura básica, por ejemplo, cómo importar el bytecode en objetos Java. La capa Lang contiene la sintaxis para especificar reglas arquitectónicas de manera concisa. Y por último, la capa Library contiene un conjunto de reglas predefinidas más complejas.

Ya se ha descrito de qué maneras puede esta librería obtener la información necesaria para realizar las verificaciones sobre el código implementado. Ahora, ¿qué aspectos de la arquitectura se pueden comprobar con ArchUnit? A continuación se muestran algunas de ellas:

En los escenarios de uso que vienen a continuación se mostrarán diferentes pruebas que ilustran algunas de estas comprobaciones, todas ellas aplicadas sobre un pequeño ejemplo de un microservicio.

Para aquel que quiera conocer todos los entresijos de la librería, recomiendo leer el apartado correspondiente en la guía de usuario, Ideas and Concepts, que se puede encontrar en este enlace.

Escenarios de uso

En este apartado se mostrará de forma práctica, mediante ejemplos de test unitarios, diferentes reglas de ArchUnit que se pueden crear para realizar aserciones sobre la arquitectura de nuestro sistema. Todo el código fuente puede ser descargado de este repositorio.

En la siguiente imagen, se muestra la estructura de paquetes del microservicio de ejemplo:

Estructura de la aplicación
Estructura de la aplicación

Por otro lado, la aplicación se enfoca en una arquitectura por capas (layered architecture).

Arquitectura por capas (Layered architecture)
Arquitectura por capas (Layered architecture)

Como se aprecia en la imagen, la capa Web se comunica directamente con la capa Service y, a su vez, esta se comunica con la capa Repository. Este es uno de los aspectos que se podrán probar mediante tests de ArchUnit, ya que el módulo Library permite realizar aserciones sobre arquitecturas típicas, como puede ser una arquitectura por capas.

Una vez introducida la estructura de la aplicación, voy a mostrar algunas de las pruebas unitarias que podemos escribir con ArchUnit y que van a ayudar a verificar que nuestro sistema cumple con las reglas arquitectónicas necesarias.

Configurar ArchUnit en el proyecto

La configuración del proyecto para poder usar ArchUnit es bastante sencilla, si por ejemplo, tu proyecto utiliza Maven, es tan sencillo como añadir la siguiente dependencia al fichero pom.xml:

<dependency>
        <groupId>com.tngtech.archunit</groupId>
        <artifactId>archunit</artifactId>
        <version>0.14.1</version>
        <scope>test</scope>
</dependency>

Esta es toda la configuración necesaria y con esto ya se pueden crear diferentes tests.

Primeros tests con ArchUnit

En puntos anteriores, se mencionó el conjunto de posibilidades que nos ofrece la librería para realizar verificaciones sobre el código implementado. En los apartados posteriores se van a mostrar diferentes casos de uso y cómo se implementan estos mediante aserciones con ArchUnit.

Antes de pasar a ver las diferentes comprobaciones que podemos realizar, voy a comentar la infraestructura que proporciona la librería para poder cargar el Java bytecode en unas estructuras Java. Este paso es necesario a la hora de realizar los tests, puesto que las reglas que se crean son evaluadas contra este conjunto de clases importadas.

Para ello, se hace uso el módulo Core de ArchUnit y con la siguiente instrucción:

JavaClasses classes = new ClassFileImporter().importPackages("com.paradigmadigital.archunit”);

El objeto devuelto, JavaClasses, representa una colección de elementos de tipo JavaClass, donde JavaClass a su vez representa un único fichero de clase importado (un fichero .class). En este ejemplo, en concreto, se importarán todos los paquetes que cuelgan de ‘com.paradigmadigital.archunit’, es decir, todas las clases de la aplicación en concreto.

Comprobaciones sobre clases:

En este escenario vamos a plantear algunos test que realicen aserciones sobre las clases del sistema en diferentes aspectos.

ArchRule rule1 = classes()
                                .that().haveSimpleNameEndingWith("Controller")
                                .should().resideInAPackage("..controller");
ArchRule rule2 = classes()
                                .that().resideInAPackage("..repository")
                                .should().beInterfaces();
ArchRule entityRule = fields()
                                .should().bePrivate();

Para evaluar cada una de las reglas (ArchRule) construidas contra un conjunto de clases que han sido importadas con la instrucción del apartado anterior, se emplea la sentencia:

<nombre_regla>.check(classes);

Comprobaciones sobre las capas y dependencias entre paquetes:

En este escenario vamos a plantear algunos test que realicen aserciones sobre las capas software que componen el sistema.

ArchRule controllerRule = classes()
                                        .that()
                                        .resideInAPackage("..controller..")
                                        .should().dependOnClassesThat()
                                        .resideInAPackage("..service..");
ArchRule serviceRule = classes()
                                        .that()
                                        .resideInAPackage("..service.impl")
                                        .should().dependOnClassesThat()
                                        .resideInAPackage("..repository..");
LayeredArchitecture architecture = layeredArchitecture()
.layer("controller").definedBy("..controller..")
.layer("service").definedBy("..service.impl")
.layer("persistence").definedBy("..repository..")
.optionalLayer("mapper").definedBy("..mapper.impl")
.whereLayer("controller").mayNotBeAccessedByAnyLayer()
.whereLayer("service").mayOnlyBeAccessedByLayers("controller")
.whereLayer("persistence").mayOnlyBeAccessedByLayers("service","mapper );

El módulo Library de ArchUnit ofrece una amplia colección de reglas predefinidas, que ofrecen soluciones para patrones más complejos pero comunes, como una arquitectura por capas, como en este ejemplo, o una arquitectura hexagonal.

Comprobaciones sobre los métodos de una clase:

También podemos realizar comprobaciones sobre los métodos existentes en determinadas clases. Por ejemplo, para comprobar si los métodos residentes en las clases del paquete controller son públicos y devuelven un determinado tipo de respuesta, se puede crear la siguiente regla:

ArchRule serviceRule = methods()
                                      .that().areDeclaredInClassesThat()
                                      .resideInAPackage("controllerl")
                                      .and().arePublic()
                                      .should().haveRawReturnedType("ResponseEntity.class")

Comprobaciones sobre convenciones de codificación:

Entre el conjunto de reglas predefinidas del módulo Library se encuentran las que hacen referencia a convenciones de codificación. Estas reglas se encuentran definidas como constantes en la clase ‘com.tngtech.archunit.library.GeneralCodingRules’.

En este escenario, se plantean algunas comprobaciones que se pueden realizar.

private final ArchRule no_generic_exceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;
private final ArchRule no_field_injection = NO_CLASSES_SHOULD_USE_FIELD_INJECTION;

Creación de reglas personalizadas:

ArchUnit permite la creación de reglas personalizadas. Si se da el caso en que se quiere construir una regla de ArchUnit, y el API predefinido no permite expresar la idea buscada, es posible extender dicho comportamiento de manera custom.

La mayor parte de las reglas que se crean siguen el patrón:


 “las clases que <PREDICADO> deberían <CONDICIÓN>”,

Que en otras palabras viene a indicar, que un conjunto de clases delimitadas por el predicado "PREDICADO" son evaluadas contra la condición "CONDICIÓN". ArchUnit permite justamente crear una definición de estos conceptos, por medio de las estructuras Java, DescribedPredicate y ArchCondition.

Por ejemplo, para crear la siguiente regla personalizada:

“las clases que tienen métodos anotados con @RequestMapping, deberían ser métodos anotados con la anotación @Secured”, 

Hay que crear una implementación Java del concepto DescribedPredicate (limita el conjunto de clases a evaluar) y del concepto ArchCondition (condición sobre la que queremos evaluar el conjunto de clases). Se podría hacer de la siguiente forma:

DescribedPredicate<JavaClass> haveAMethodAnnotatedWithRequestMapping =
    new DescribedPredicate<JavaClass>("method annotated with @RequestMapping"){
        @Override
        public boolean apply(JavaClass input) {
    boolean someMethodAnnotatedWithRequestMapping;
            // Se itera sobre los métodos y se comprueba la anotación @RequestMapping
            return someMethodAnnotatedWithRequestMapping;
        }
    };

ArchCondition<JavaClass> beAnnotatedBySecuredTag =
    new ArchCondition<JavaClass>("annotated by @Secured" tag) {
        @Override
        public void check(JavaClass item, ConditionEvents events) {
            for (JavaMethodCall call : item.getMethodCallsToSelf()) {
                if (!call.getOrigin().isAnnotatedWith(Secured.class)) {
                    String message = String.format(
                        "Method %s is not @Secured", call.getOrigin().getFullName());
                    events.add(SimpleConditionEvent.violated(call, message));
                }
            }
        }
    };

Posteriormente, se puede utilizar el código implementado para construir una regla de ArchUnit como hemos visto hasta ahora:

ArchRule rule1 = classes().that(haveAMethodAnnotatedWithRequestMapping)
.should(beAnnotatedBySecuredTag);

Llegados a este punto, se han mostrado diversos escenarios de uso de la librería ArchUnit, pero esto no es todo lo que puede ofrecer. Existen otras funcionalidades interesantes que la librería puede ofrecer, como la integración con PlantUML. PlantUML es una herramienta software que permite escribir diferentes tipos de diagramas UML como diagramas de clases, casos de uso, etc. Pues bien, ArchUnit puede derivar reglas directamente de los diagramas de PlantUML y comprobar que todas las clases Java importadas cumplen con las dependencias del diagrama.

En definitiva, intentar contar todo el API en detalle llevaría quizás varios posts. Para terminar, me gustaría dejar un par de enlaces para aquellos que deseen seguir indagando en todas las posibilidades de esta librería:

Conclusión

Cuando se está desarrollando software, es muy importante implementar una verificación automática de la arquitectura del sistema, ya que estas pruebas pueden ayudar a reducir la deuda técnica que pueda generarse y puede ayudar a monitorizar y verificar el progreso hacia la arquitectura óptima del producto.

Una idea que puede resultar de interés para cualquier compañía del sector TI, sería el de poder disponer de diferentes suites de test ArchUnit, desarrollados para diferentes arquitecturas, garantizando así que todos los desarrollos cumplan con los objetivos fijados inicialmente y reduciendo el esfuerzo extra que habría que dedicar en cada implementación de un proyecto. Como se ha comentado a lo largo del post, estos test pueden ser incluidos en el ciclo de integración continua, ejecutándose de manera automática y proporcionando un feedback inmediato durante la fase de desarrollo.

Dicho esto, aunque existen diferentes herramientas en el mercado que pueden ser utilizadas para testear entre otras cosas, dependencias entre paquetes y clases, ArchUnit proporciona un API bastante amplio con el que se pueden construir todo tipo de reglas de una manera sencilla y concisa. Además, se trata de una librería extensible, es decir, ofrece la posibilidad de poder crear reglas personalizadas para aquellas aserciones que se quieran realizar y no estén contempladas en el API de ArchUnit.

Cuéntanos qué te parece.

Enviar.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete