Pruebas unitarias en Java Spring Boot, sobre un proyecto con Arquitectura Hexagonal

Pruebas unitarias en Java Spring Boot, sobre un proyecto con Arquitectura Hexagonal


Las pruebas unitarias son una parte fundamental del desarrollo de software para garantizar la calidad, eficiencia y estabilidad del código. En este artículo explicaremos cómo implementar pruebas unitarias en un proyecto de Java Spring Boot con Arquitectura Hexagonal utilizando JUnit 5 y Mockito aplicando buenas prácticas y explicando desde esta arquitectura como podemos realizar pruebas unitarias de forma efectiva.

¿Qué es Spring Boot?

Spring Boot es un framework de aplicaciones Java que proporciona una manera fácil y rápida de crear aplicaciones empresariales con una configuración mínima. Spring Boot utiliza la arquitectura de Spring para automatizar la configuración y reducir la cantidad de código requerido, lo que permite a los desarrolladores enfocarse en el desarrollo de características y funcionalidades. Además, Spring Boot incluye una serie de herramientas y módulos que facilitan la implementación de características comunes en aplicaciones empresariales, como seguridad, monitoreo, persistencia de datos y más.

¿Qué es Arquitectura Hexagonal?

La “Arquitectura Hexagonal” es un patrón de diseño de software que se centra en la separación clara entre la lógica del negocio y la interfaz con el usuario. La idea es que la lógica del negocio esté contenida en un núcleo central, llamado "hexágono", que es independiente de la forma en que se comunica con el usuario o con otros sistemas externos. De esta manera, se pueden realizar cambios en la interfaz sin afectar el núcleo del sistema, y viceversa. La arquitectura hexagonal fomenta la reutilización y la mantenibilidad del código, y permite desarrollar aplicaciones escalables y adaptables a diferentes necesidades.

Pruebas unitarias sobre Arquitectura Hexagonal

En la Arquitectura Hexagonal, son esenciales para asegurar la integridad y la calidad del software. Las pruebas unitarias se utilizan para probar las partes centrales del sistema, conocido como "hexágono", que contiene la lógica del negocio.

A continuación mostraremos una imagen con el encarpetado en Arquitectura Hexagonal.


El objetivo de las pruebas unitarias en una arquitectura hexagonal es asegurarse de que el hexágono funcione correctamente de manera aislada, sin tener que depender de la interfaz con el usuario o de otros sistemas externos. Esto permite a los desarrolladores realizar cambios en la interfaz o en los sistemas externos sin afectar el comportamiento del hexágono, y viceversa.

¿Qué es JUnit?

“JUnit” es un Framework Open Source para la automatización de pruebas (tanto unitarias, como de integración) en los proyectos de Software. El framework provee al usuario de herramientas, clases y métodos que le facilitan la tarea de realizar pruebas en su sistema y así asegurar su consistencia y funcionalidad del desarrollador, la estabilidad del código y el tiempo dedicado a la depuración. Su última versión, JUnit 5 ha sido completamente reescrita y cuenta con una serie de nuevas características y mejoras.

¿Qué es una prueba unitaria?

Las pruebas unitarias son una técnica de desarrollo de software que consiste en probar pequeñas unidades de código para asegurar que funcionan correctamente. Estas pruebas son esenciales para garantizar la calidad del software y facilitar el mantenimiento y la actualización. Al realizar pruebas unitarias, Se pueden detectar y corregir errores temprano en el proceso de desarrollo, lo que reduce el tiempo y los costos necesarios para resolver problemas en etapas posteriores.

Ventajas de las pruebas Unitarias

  1. Detectar errores temprano: Al ejecutar pruebas unitarias frecuentemente, se pueden detectar errores en el código desde una etapa temprana, lo que facilita la corrección antes de que el software se publique.

  2. Mejora la calidad del software: Las pruebas unitarias aseguran que el código siga funcionando correctamente después de cada cambio, lo que ayuda a mejorar la calidad del software.

  3. Facilita el desarrollo incremental: Al tener pruebas automatizadas, se puede agregar o cambiar funcionalidades con más confianza, ya que las pruebas unitarias validan que el código siga funcionando correctamente.

  4. Reducción de costos: Al detectar errores temprano, las pruebas unitarias pueden ayudar a reducir los costos de corrección en comparación con la detección de errores en etapas posteriores del desarrollo.

  5. Facilita el mantenimiento: Al tener pruebas automatizadas, es más fácil mantener el software a lo largo del tiempo y realizar cambios con confianza.

En resumen, las pruebas unitarias son una herramienta esencial para mejorar la calidad y la confiabilidad del software, para reducir el costo y el tiempo de desarrollo.

¿Qué es Mockito?

“Mockito” es una biblioteca de pruebas para Java que permite crear objetos simulados (o mocks) de clases e interfaces. Estos objetos simulados se utilizan para probar el comportamiento de una clase en particular, ya sea de forma aislada o en combinación con otras clases. Con Mockito, puedes simular comportamientos específicos de una clase, como la llamada a un método o el lanzamiento de una excepción, para probar cómo tu código reacciona a esos eventos. También puedes verificar que se hayan realizado ciertas acciones, como llamadas a métodos específicos.

¿Qué es un dominio?

El término "dominio" se refiere a la parte central del sistema que contiene la lógica del negocio. Es el "corazón" de la arquitectura y se representa como un hexágono en el diagrama de la arquitectura. El dominio es responsable de implementar las reglas y los procesos de negocio y de mantener la información y el estado necesario para cumplir con los requisitos del sistema. Esta información y estado son aislados del resto del sistema y no dependen de la interfaz con el usuario o de otros sistemas externos.

¿Qué es un servicio?

Un servicio es un componente que proporciona una interfaz para interactuar con el dominio o parte central del sistema que contiene la lógica del negocio. Los servicios actúan como intermediarios entre el dominio y las aplicaciones externas, y son responsables de realizar tareas específicas en nombre de las aplicaciones externas.

Anotaciones y métodos más utilizados en las pruebas unitarias sobre Dominios y Servicios:

  • @TestFactory debe devolver un Stream , Collection , Iterable o Iterator de instancias de DynamicTest . Devolver cualquier otra cosa dará como resultado una excepción JUnitException ya que los tipos de devolución no válidos no se pueden detectar en el momento de la compilación. Aparte de esto, un método @TestFactory no puede ser estático o privado

  • @DisplayName declaramos los nombres personalizados para las clases y métodos de prueba.

  • @MockBean de Spring Boot, agregamos objetos simulados al contexto de la aplicación, el simulacro reemplazará cualquier valor o dato existente del mismo tipo en la aplicación.

  • @Autowired esta anotación es una de las más importantes cuando trabajamos con spring framework, ya que permite inyectar unas dependencias con otras dentro de spring.

  • @BeforeEach identifica el método que se ejecutará antes de cada prueba, esto nos permite realizar configuraciones necesarias antes de la ejecución de cada prueba individual.

  • ThrowingSupplieres una interfaz funcional que se puede usar para implementar cualquier bloque genérico de código que devuelve un objeto y potencialmente arroja un Throwable.

  • NullPointerException si dicho valor o formato es nulo, automáticamente lanzará la excepción.

  • IllegalArgumentException si dicho valor es vacío o no hay argumentos suficientes también lanzará la excepción.

  • Assertions Las afirmaciones de clase juegan un papel muy importante en los test de pruebas unitarias, es una colección de métodos de utilidad que admiten la afirmación de condiciones en las pruebas, en caso de que no cumpla la condición esta fallará y arrojara un assertionFailedError. Los métodos y excepciones más utilizados en las pruebas de dominios y servicios son los siguientes:

  • assertAll afirman que todos los ejecutables no arrojan excepciones.

  • assertDoesNotThrow afirma que la ejecución de lo suministrado no arroja ninguna excepción.

  • assertNotNull comprueban que el valor esperado no sea nulo.

  • assertThrows si no se lanza ninguna excepción, o si se lanza una excepción de un tipo diferente, este método fallará.

  • assertFalse verifica que el elemento no se encuentre vacío.

  • assertEquals afirma que el valor esperado sea igual al actual.

En el siguiente link podrás hachar un vistazo a JUnit con todos los métodos a utilizar en pruebas unitarias con JUnit 5.

¿Por qué solo haremos pruebas unitarias sobre dominios y servicios?

En un proyecto de Java Spring Boot las pruebas unitarias se enfocan principalmente en las capas de dominio y servicio debido a que estas son las capas que contienen la lógica de negocio y deben ser probadas exhaustivamente para garantizar la calidad, eficiencia y estabilidad de una aplicación. Las pruebas unitarias en estas capas pueden ayudar a detectar errores de forma temprana durante el desarrollo, lo que reduce el tiempo y el costo en resolver problemas a futuro en una etapa avanzada del proyecto.

¿Cómo hacer una prueba de Dominio?

Este código es una clase de Java llamada "AccessToken" que implementa la interfaz "String Serializable". La clase tiene una propiedad "value" de tipo String y un constructor que toma un valor de tipo String. El constructor utiliza las clases "Preconditions" y "StringUtils" para verificar que el valor proporcionado no sea nulo ni una cadena vacía. La clase también tiene un método "valueOf()" que devuelve el valor de la propiedad "value". La anotación "@Value(staticConstructor = "of")" es de lombok y es utilizado para generar un método estático "of" que retorna una nueva instancia de la clase.

@Value(staticConstructor = "of")
public class AccessToken implements StringSerializable {
    String value;
    public AccessToken(String value) {
        Preconditions.checkNotNull(value, "AccessToken can not be null");
        Preconditions.checkArgument(StringUtils.isNotBlank(value), "AccessToken can not be blank");
        this.value = value;
    }
    @Override
    public String valueOf() {
        return value;
    }
}

Es recomendable que los test o pruebas unitarias se hagan de forma individual para evitar errores al momento de correrlos, veremos 3 ejemplos de como hacerlos de forma correcta:

  1. itShouldPass(), es una prueba dinámica que crea varios casos de prueba para un token de acceso válido. Utiliza el método Stream.of() para crear un flujo de valores de token de acceso y luego utiliza el método map() para crear una prueba dinámica para cada valor en el flujo, cada prueba dinámica se asegura de que el token de acceso no lanza una excepción al crearlo, no es nulo y no está vacío.

 @TestFactory
  @DisplayName("It should create valid accessToken")
  Stream<DynamicTest> itShouldPass() {
      //arrange
      return Stream.of(
              "**************************************"
      ).map(accessToken -> {
          String testName = String.format("%s should be valid for accessToken", accessToken);
          //act
          Executable executable = () -> {
              ThrowingSupplier<AccessToken> throwingSupplier = () -> AccessToken.of(accessToken);
              //assert
              assertAll(
                      () -> assertDoesNotThrow(throwingSupplier),
                      () -> assertNotNull(throwingSupplier.get()),
                      () -> assertFalse(throwingSupplier.get().getValue().isEmpty())
              );
          };
          return DynamicTest.dynamicTest(testName, executable);
      });
  }
  1. itShouldNotPass(), se asegura de que se lancen excepciones apropiadas cuando se intenta crear un token de acceso con un valor nulo o vacío.

    @Test
    @DisplayName("It should not create accessToken for invalid case")
    void itShouldNotPass() {
        String a1 = null;
        String a2 = "";
        assertAll(
                () -> assertThrows(NullPointerException.class, () -> AccessToken.of(a1)),
                () -> assertThrows(IllegalArgumentException.class, () -> AccessToken.of(a2))
        );
    }
  2. valueOfSameValue(), se asegura de que el método valueOf() de la clase AccessToken devuelve el valor del token de acceso que se utilizó para crear la instancia.

    @Test
    @DisplayName("Value of returns same value entered for accessToken")
    void valueOfSameValue() {
        String accessToken = "**************************";
        AccessToken accessTokenInstance = AccessToken.of(accessToken);
        String message = String.format("Expecting to return value %s for de instance %s",
                accessToken,accessTokenInstance.valueOf());
        assertEquals(accessToken,accessTokenInstance.valueOf(),message);
    }

¿Cómo hacer una prueba de Servicio?

La clase "FindCitiesByDepartmentService" implementa la interfaz "FindCitiesByDepartmentUseCase". El constructor de esta clase es anotado con @RequiredArgsConstructor lo que genera automaticamente un constructor con todos los argumentos final y no nulos. La clase tiene una dependencia "findCitiesByDepartmentPort" que es una instancia de "FindCitiesByDepartmentPort" y es inyectada en el constructor. El método "findCitiesByDepartment" toma una consulta "FindCitiesByDepartmentQuery" y utiliza el método "findCitiesByDepartment" del "findCitiesByDepartmentPort" para obtener una lista de "City" con el id enviado en la consulta. El resultado es devuelto en un objeto "Try" que es un contenedor para un resultado exitoso o un error.

@UseCase
@RequiredArgsConstructor
public class FindCitiesByDepartmentService implements FindCitiesByDepartmentUseCase {
    private final FindCitiesByDepartmentPort findCitiesByDepartmentPort;
    @Override
    public Try<List<City>> findCitiesByDepartment(FindCitiesByDepartmentQuery query) {
        return findCitiesByDepartmentPort.findCitiesByDepartment(query.getId());
    }
}

Los test o pruebas unitarias de servicios nos ayudan a probar la correcta implementación de un servicio, estos tests identifican errores en la lógica del servicio, y se aseguran que cumplan con los requisitos y expectativas. Los desarrolladores podemos verificar que nuestro código funcione de una manera efectiva antes de implementarlo en el entorno de producción. A continuación implementaremos la inyección de dependencias y también la forma correcta de hacer pruebas unitarias.

class FindCitiesByDepartmentServiceTest {
    @MockBean
    FindCitiesByDepartmentPort findCitiesByDepartmentPort;
    @Autowired
    FindCitiesByDepartmentService findCitiesByDepartmentService;
    @BeforeEach
    void setUp() {
        findCitiesByDepartmentPort = mock(FindCitiesByDepartmentPort.class);
        findCitiesByDepartmentService = new FindCitiesByDepartmentService(findCitiesByDepartmentPort);
    }
  1. “El valor esperado debe ser igual al valor actual”, el método when devuelve un valor predeterminado, este invoca el método que deseamos probar y validar los resultados con el assertEquals. Por último verificamos que dicho método sólo sea llamado una vez.

  @Test
    @DisplayName("Expected value equal to actual value")
    void valueEqualToValue() {
        //Given - aparece antes de la ejecucion
        when(findCitiesByDepartmentPort.findCitiesByDepartment(DepartmentId.of(1))).thenReturn(getDataResponse());
        //when - cuando ejecutamos el metodo que queremos probar
        Try<List<City>> test = findCitiesByDepartmentPort.findCitiesByDepartment(DepartmentId.of(1));
        //them - entonces validamos
        assertEquals("Antioquia",test.get().get(0).getCityName().getValue(),"IT'S NOT EQUALS");
        assertEquals("64",test.get().get(0).getCode().getValue(),"IT'S NOT EQUALS");
        verify(findCitiesByDepartmentPort,times(1)).findCitiesByDepartment(DepartmentId.of(1));
    }
  1. “Que la Lista no sea vacía”, el método when simula una llamada al método findCitiesByDepartment y devuelve los datos definidos en el método estático getDataResponse() que estará al final de los test, el método assertFalse verifica que la lista devuelta esté vacía y en caso contrario, imprimirá un mensaje "IT'S EMPTY". por último verificamos que dicho método sólo sea llamado una vez.

  @Test
    @DisplayName("Empty list value")
    void emptyValue() {
        when(findCitiesByDepartmentPort.findCitiesByDepartment(DepartmentId.of(1)))
                .thenReturn(getDataResponse());
        Try<List<City>> cities = findCitiesByDepartmentPort.findCitiesByDepartment(DepartmentId.of(1));
        assertFalse(cities.get().isEmpty(),"IT'S EMPTY");
        verify(findCitiesByDepartmentPort,times(1)).findCitiesByDepartment(DepartmentId.of(1));
    }
  1. “Que el valor actual no sea nulo” en este test verificamos que los valores obtenidos del método findCitiesByDepartment no son nulos.

  @Test
    @DisplayName("Null current value")
    void valueNull() {
        when(findCitiesByDepartmentPort.findCitiesByDepartment(DepartmentId.of(1))).thenReturn(getDataResponse());
        Try<List<City>> cities = findCitiesByDepartmentPort.findCitiesByDepartment(DepartmentId.of(1));
        assertNotNull(cities.get().get(0).getCityName().getValue(),"IT'S NULL");
        assertNotNull(cities.get().get(0).getDepartmentId().getValue(),"IT'S NULL");
        verify(findCitiesByDepartmentPort,times(1)).findCitiesByDepartment(DepartmentId.of(1));
    }

Para finalizar con los test de servicios mostraré un método estático, el cual es implementado para tener un código más limpio en los test. En este método simulamos la creación de una lista e insertamos valores predeterminados los cuales simularemos en los test. en ellos esperamos que los tipos de datos sean los correctos y que cuente con todos los atributos que pide el método.

  static Try<List<City>> getDataResponse(){
        return Try.of(() -> {
            List<City> cityList = new ArrayList<>();
            cityList.add(City.builder()
                    .id(CityId.of(1))
                    .cityName(CityName.of("Antioquia"))
                    .code(Code.of("64"))
                    .departmentId(DepartmentId.of(1))
                    .build());
            return cityList;
        });
    }

El artículo trata sobre la implementación de las pruebas unitarias en el desarrollo de software y su detección temprana de posibles errores para garantizar un código de calidad y eficiente. Se muestra como utilizar herramientas como JUnit y Mockito para realizar pruebas unitarias en un contexto de Arquitectura Hexagonal, se concluye que la combinación de estas dos herramientas son una buena práctica que ayuda a mejorar la calidad del software para desarrollar aplicaciones más robustas. Si deseas conocer más información acerca de estas dos herramientas te invito a visitar sus páginas web en los siguientes links:

JUnit 5 y Mockito.

Articulo Elaborado Por:

Harol Eduardo Ardila Palacios

Desarrollador de software en

I.A.S. Ingeniería, Aplicaciones y Soluciones S.A.S.



Angular Routing