Unit tests para la arquitectura de Software

16 de abril de 2020

Las revisiones de código son una práctica importante para garantizar la calidad de los proyectos de software. Deben ser tomadas en serio y los revisores deben tratar de revisar el código tan pronto como sea posible para minimizar el coste para los desarrolladores del cambio de contexto.

Pero como amigo de la automatización y la eficiencia, muchas veces me parece innecesario ver discusiones sobre temas triviales en las revisiones de código.

El problema

Considera esta pull ficticia para un proyecto Java y la discusión durante su revisión:

Pull request:

package com.acme.domain.service;
import com.acme.infrastructure.repository.MongoFooRepository;

@ApplicationService
class FooService {
  public MongoFooRepository fooRepository;
  
  String retrieveBar(Integer barId)
  {
    return fooRepository.get(barId).orElseThrow(() -> new RuntimeException("not found")); 
  }
}

Code review:

Reviewer A: Please move opening brace on the same line as the method declaration.

Autor: Sorry, my bad, I'm still used to C# style braces...

Reviewer B: We use 4 spaces for indentation, not 2.

Autor: Might as well use tabs, while we're at it..?

Reviewer B: We decided to use spaces to assure the indentations have a uniform width everywhere.

Autor: I think tabs might be better because it's more accessible for visually impaired people...

Reviewer B: ...🤔

Reviewer C:

  • ApplicationServices should be suffixed with ApplicationService, not just Service.
  • The core domain should not have any dependencies on the infrastructure, therefore you should not use the concrete Mongo repository implementation directly. Use an interface instead. Note that the field should be private, not public.
  • Lastly we should not use generic exceptions like RuntimeException but use or create a more specific one instead.

Autor: Ok.

Reviewer D: Why use Integer instead of int?

Autor: Because the value might be null.

Reviewer D: Then please add a null check.

Autor: Can I also use Optional?

Reviewer D: Optionals are not supposed to be used as parameters.

...

Inefficiency level: over 9000

Ugh...😵

La solución

Afortunadamente, existe una amplia variedad de herramientas que ayudan a hacer cumplir ciertas directrices y normas para los proyectos. La mayoría de las cuestiones mencionadas por los examinadores A, B y D pueden evitarse por completo utilizando analizadores de código estático, formateadores de código, linters, etc. Pueden configurarse en el IDE, integrarse en un hook git o en algún lugar del proceso de compilación para hacer que la compilación falle cuando no se siga ninguna política.

Sin embargo, las preocupaciones del Revisor C son legítimas y no pueden evitarse realmente con las herramientas mencionadas anteriormente. Este es el momento que entra ArchUnit.

ArchUnit es una biblioteca para probar la arquitectura del código de los proyectos Java utilizando simples pruebas unitarias. Veamos cómo funciona.

Primero, agrega la dependencia y una nueva clase para nuestras pruebas.

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit</artifactId>
    <version>0.12.0</version>
    <scope>test</scope>
</dependency>
@AnalyzeClasses(packages = "com.acme", importOptions = {
    ImportOption.DoNotIncludeTests.class,
    ImportOption.DoNotIncludeJars.class
})
public class AcmeArchitectureTest {
   // TODO: That's were we will add our ArchTests
}

Ahora, abordemos las preocupaciones del revisor C una por una añadiendo simples @ArchTests a la clase de prueba.

Preocupación #1: Las clases anotadas con ApplicationService deben tener el sufijo apropiado:

@ArchTest
public static final ArchRule applicationServicesShouldHaveProperSuffix = ArchRuleDefinition
        .classes().that().areAnnotatedWith(ApplicationService.class)
        .should().haveSimpleNameEndingWith("ApplicationService");

Ya que estamos en eso, agreguemos una prueba para asegurarnos que todos los servicios de aplicación residen en el paquete apropiado.

@ArchTest
public static final ArchRule applicationServicesShouldResideInApplicationPackage = ArchRuleDefinition
        .classes().that().areAnnotatedWith(ApplicationService.class)
        .should().resideInAPackage("com.acme.application");

Preocupación #2: No hay dependencias desde el núcleo hasta la infraestructura

@ArchTest
public static final ArchRule domainShouldNotHaveAnyDependenciesOtherThanJdkAndItself = ArchRuleDefinition
        .classes().that().resideInAPackage("com.acme.domain..")
        .should().onlyDependOnClassesThat().resideInAnyPackage("com.acme.domain..", "java..");

Preocupación #3: Los campos deben ser privados

public static final ArchRule nonStaticFieldsShouldBePrivate = ArchRuleDefinition
        .fields().that().areNotStatic()
        .should().bePrivate();

Preocupación #4: No hay excepciones genéricas:

@ArchTest
private final ArchRule noGenericExceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;

...y eso es todo... ¡así de fácil!

Resumen

Con ArchUnit podemos asegurar que ciertos patrones y reglas arquitectónicas predefinidas son seguidas por todos los que trabajan en el proyecto. El incumplimiento de las pruebas de la ArchUnit, por supuesto, hará que el build fracase. Como vemos en los ejemplos anteriores, ArchUnit tiene una API muy fácil semánticamente. Usando el IDEs Usando en el IDE IntelliSense, apenas necesitamos leer la documentación 😃. Echa un vistazo a these examples on Github para obtener más inspiración.

Sobre el autor: Patric Steiner

Soy un entusiasta del Machine Learning, me encantan las nuevas tecnologías y me divierte la programación declarativa.

Comments
Únete a nosotros