Tutorial de JUnit 5, parte 2: Prueba unitaria Spring MVC con JUnit 5

Spring MVC es uno de los frameworks Java más populares para crear aplicaciones Java empresariales y se presta muy bien a las pruebas. Por diseño, Spring MVC promueve la separación de preocupaciones y fomenta la codificación contra interfaces. Estas cualidades, junto con la implementación de Spring de la inyección de dependencias, hacen que las aplicaciones Spring sean muy probables.

Este tutorial es la segunda mitad de mi introducción a las pruebas unitarias con JUnit 5. Le mostraré cómo integrar JUnit 5 con Spring, luego le presentaré tres herramientas que puede usar para probar los controladores, servicios y repositorios Spring MVC.

descargar Obtener el código Descargar el código fuente, por ejemplo, las aplicaciones utilizadas en este tutorial. Creado por Steven Haines para JavaWorld.

Integrando JUnit 5 con Spring 5

Para este tutorial, estamos usando Maven y Spring Boot, por lo que lo primero que debemos hacer es agregar la dependencia JUnit 5 a nuestro archivo Maven POM:

  org.junit.jupiter junit-jupiter 5.6.0 test  

Al igual que hicimos en la Parte 1, usaremos Mockito para este ejemplo. Entonces, vamos a necesitar agregar la biblioteca JUnit 5 Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

@ExtendWith y la clase SpringExtension

JUnit 5 define una interfaz de extensión , a través de la cual las clases pueden integrarse con las pruebas de JUnit en varias etapas del ciclo de vida de ejecución. Podemos habilitar extensiones agregando la @ExtendWithanotación a nuestras clases de prueba y especificando la clase de extensión a cargar. La extensión puede implementar varias interfaces de devolución de llamada, que se invocarán durante todo el ciclo de vida de la prueba: antes de que se ejecuten todas las pruebas, antes de que se ejecuten todas las pruebas, después de que se ejecuten todas las pruebas y después de que se hayan ejecutado todas las pruebas.

Spring define una SpringExtensionclase que se suscribe a las notificaciones del ciclo de vida de JUnit 5 para crear y mantener un "contexto de prueba". Recuerde que el contexto de la aplicación de Spring contiene todos los beans de Spring en una aplicación y que realiza una inyección de dependencia para conectar una aplicación y sus dependencias. Spring usa el modelo de extensión JUnit 5 para mantener el contexto de la aplicación de la prueba, lo que hace que escribir pruebas unitarias con Spring sea sencillo.

Después de agregar la biblioteca JUnit 5 a nuestro archivo POM de Maven, podemos usar SpringExtension.classpara extender nuestras clases de prueba JUnit 5:

 @ExtendWith(SpringExtension.class) class MyTests { // ... }

El ejemplo, en este caso, es una aplicación Spring Boot. Afortunadamente, la @SpringBootTestanotación ya incluye la @ExtendWith(SpringExtension.class)anotación, por lo que solo necesitamos incluirla @SpringBootTest.

Añadiendo la dependencia de Mockito

Para probar adecuadamente cada componente de forma aislada y simular diferentes escenarios, vamos a querer crear implementaciones simuladas de las dependencias de cada clase. Aquí es donde entra Mockito. Incluya la siguiente dependencia en su archivo POM para agregar soporte para Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

Después de haber integrado JUnit 5 y Mockito en su aplicación Spring, puede aprovechar Mockito simplemente definiendo un bean Spring (como un servicio o repositorio) en su clase de prueba usando la @MockBeananotación. Aquí está nuestro ejemplo:

 @SpringBootTest public class WidgetServiceTest { /** * Autowire in the service we want to test */ @Autowired private WidgetService service; /** * Create a mock implementation of the WidgetRepository */ @MockBean private WidgetRepository repository; ... } 

En este ejemplo, estamos creando una simulación WidgetRepositorydentro de nuestra WidgetServiceTestclase. Cuando Spring vea esto, lo conectará automáticamente a nuestro WidgetServicepara que podamos crear diferentes escenarios en nuestros métodos de prueba. Cada método de prueba configurará el comportamiento del WidgetRepository, como devolver lo solicitado Widgeto devolver un Optional.empty()para una consulta para la que no se encuentran los datos. Pasaremos el resto de este tutorial mirando ejemplos de varias formas de configurar estos frijoles simulados.

La aplicación de ejemplo Spring MVC

Para escribir pruebas unitarias basadas en Spring, necesitamos una aplicación para escribirlas. Afortunadamente, podemos usar la aplicación de ejemplo de mi tutorial de Spring Series "Mastering Spring framework 5, Part 1: Spring MVC". Usé la aplicación de ejemplo de ese tutorial como aplicación base. Lo modifiqué con una API REST más fuerte para que tuviéramos algunas cosas más para probar.

La aplicación de ejemplo es una aplicación web Spring MVC con un controlador REST, una capa de servicio y un repositorio que usa Spring Data JPA para conservar "widgets" hacia y desde una base de datos H2 en memoria. La figura 1 es una descripción general.

Steven Haines

¿Qué es un widget?

A Widgetes simplemente una "cosa" con un ID, nombre, descripción y número de versión. En este caso, nuestro widget está anotado con anotaciones JPA para definirlo como una entidad. El WidgetRestControlleres un controlador Spring MVC que traduce las llamadas a la API RESTful en acciones para realizar Widgets. El WidgetServicees un servicio de Primavera estándar que define la funcionalidad de negocio para Widgets. Finalmente, WidgetRepositoryes una interfaz Spring Data JPA, para la cual Spring creará una implementación en tiempo de ejecución. Revisaremos el código de cada clase mientras escribimos las pruebas en las siguientes secciones.

Prueba unitaria de un servicio Spring

Comencemos revisando cómo probar un servicio Spring  , porque ese es el componente más fácil de probar en nuestra aplicación MVC. Los ejemplos de esta sección nos permitirán explorar la integración de JUnit 5 con Spring sin introducir nuevos componentes de prueba o bibliotecas, aunque lo haremos más adelante en el tutorial.

Comenzaremos revisando la WidgetServiceinterfaz y la WidgetServiceImplclase, que se muestran en el Listado 1 y el Listado 2, respectivamente.

Listado 1. La interfaz del servicio Spring (WidgetService.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import java.util.List; import java.util.Optional; public interface WidgetService { Optional findById(Long id); List findAll(); Widget save(Widget widget); void deleteById(Long id); }

Listado 2. La clase de implementación del servicio Spring (WidgetServiceImpl.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository; import com.google.common.collect.Lists; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Optional; @Service public class WidgetServiceImpl implements WidgetService { private WidgetRepository repository; public WidgetServiceImpl(WidgetRepository repository) { this.repository = repository; } @Override public Optional findById(Long id) { return repository.findById(id); } @Override public List findAll() { return Lists.newArrayList(repository.findAll()); } @Override public Widget save(Widget widget) { // Increment the version number widget.setVersion(widget.getVersion()+1); // Save the widget to the repository return repository.save(widget); } @Override public void deleteById(Long id) { repository.deleteById(id); } }

WidgetServiceImples un servicio Spring, anotado con la @Serviceanotación, que tiene un WidgetRepositorycableado a través de su constructor. Las findById(), findAll()y deleteById()los métodos son métodos de paso a través de la subyacente WidgetRepository. La única lógica empresarial que encontrará se encuentra en el save()método, que incrementa el número de versión del Widgetcuando se guarda.

La clase de prueba

Para probar esta clase, necesitamos crear y configurar un simulacro WidgetRepository, conectarlo a la WidgetServiceImplinstancia y luego conectarlo WidgetServiceImpla nuestra clase de prueba. Afortunadamente, eso es mucho más fácil de lo que parece. El Listado 3 muestra el código fuente de la WidgetServiceTestclase.

Listado 3. La clase de prueba del servicio Spring (WidgetServiceTest.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.Arrays; import java.util.List; import java.util.Optional; import static org.mockito.Mockito.doReturn; import static org.mockito.ArgumentMatchers.any; @SpringBootTest public class WidgetServiceTest { /** * Autowire in the service we want to test */ @Autowired private WidgetService service; /** * Create a mock implementation of the WidgetRepository */ @MockBean private WidgetRepository repository; @Test @DisplayName("Test findById Success") void testFindById() { // Setup our mock repository Widget widget = new Widget(1l, "Widget Name", "Description", 1); doReturn(Optional.of(widget)).when(repository).findById(1l); // Execute the service call Optional returnedWidget = service.findById(1l); // Assert the response Assertions.assertTrue(returnedWidget.isPresent(), "Widget was not found"); Assertions.assertSame(returnedWidget.get(), widget, "The widget returned was not the same as the mock"); } @Test @DisplayName("Test findById Not Found") void testFindByIdNotFound() { // Setup our mock repository doReturn(Optional.empty()).when(repository).findById(1l); // Execute the service call Optional returnedWidget = service.findById(1l); // Assert the response Assertions.assertFalse(returnedWidget.isPresent(), "Widget should not be found"); } @Test @DisplayName("Test findAll") void testFindAll() { // Setup our mock repository Widget widget1 = new Widget(1l, "Widget Name", "Description", 1); Widget widget2 = new Widget(2l, "Widget 2 Name", "Description 2", 4); doReturn(Arrays.asList(widget1, widget2)).when(repository).findAll(); // Execute the service call List widgets = service.findAll(); // Assert the response Assertions.assertEquals(2, widgets.size(), "findAll should return 2 widgets"); } @Test @DisplayName("Test save widget") void testSave() { // Setup our mock repository Widget widget = new Widget(1l, "Widget Name", "Description", 1); doReturn(widget).when(repository).save(any()); // Execute the service call Widget returnedWidget = service.save(widget); // Assert the response Assertions.assertNotNull(returnedWidget, "The saved widget should not be null"); Assertions.assertEquals(2, returnedWidget.getVersion(), "The version should be incremented"); } } 

La WidgetServiceTestclase está anotada con la @SpringBootTestanotación, que busca CLASSPATHtodas las clases de configuración de Spring y beans y configura el contexto de la aplicación Spring para la clase de prueba. Tenga en cuenta que WidgetServiceTesttambién incluye implícitamente la @ExtendWith(SpringExtension.class)anotación, a través de la @SpringBootTestanotación, que integra la clase de prueba con JUnit 5.

La clase de prueba también usa la @Autowiredanotación de Spring para conectar automáticamente a WidgetServicepara probar, y usa la @MockBeananotación de Mockito para crear una simulación WidgetRepository. En este punto, tenemos una simulación WidgetRepositoryque podemos configurar y una real WidgetServicecon la simulación WidgetRepositoryconectada.

Probando el servicio Spring

El primer método de prueba testFindById(), ejecuta WidgetServiceel findById()método, que debería devolver un Optionalque contiene un Widget. Comenzamos creando un Widgetque queremos WidgetRepositoryque regrese. Luego aprovechamos la API de Mockito para configurar el WidgetRepository::findByIdmétodo. La estructura de nuestra lógica simulada es la siguiente:

 doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD 

En este caso, estamos diciendo: Devuelve un valor Optionalde nuestro Widgetcuando findById()se llama al método del repositorio con un argumento de 1 (como a long).

A continuación, invocamos el método WidgetService's findByIdcon un argumento de 1. Luego validamos que está presente y que el devuelto Widgetes el que configuramos WidgetRepositorypara que devuelva el mock .