Seminario web destacado: Presentación de la prueba CT de Parasoft C/C++ para pruebas continuas y excelencia en el cumplimiento | Vea ahora

Por qué debería comenzar a usar Mocking para las pruebas unitarias de Spring Boot ahora mismo

Foto de cabeza de Brian McGlauflin,
Sábado, Junio 5, 2023
7 min leer

La infraestructura de prueba que ofrecen Spring Framework y Spring Boot facilita la escritura de pruebas JUnit para sus aplicaciones Spring. Lea esta publicación curada por expertos para obtener más información.

El Marco de primavera, junto con Spring Boot, proporciona un marco de prueba útil para escribir pruebas JUnit para sus aplicaciones Spring. En esta publicación, abordaré uno de los mayores desafíos de probar cualquier aplicación compleja: la gestión de dependencias.

¿Qué significa burlarse en Spring Boot?

Burlarse es un técnica utilizada en las pruebas unitarias para simular el comportamiento de objetos reales cuando la unidad que se prueba tiene dependencias externas. Las simulaciones, o simulacros, se utilizan en lugar de los objetos reales. El objetivo de burlarse es aislar y concentrarse en el código que se está probando y no en el comportamiento o estado de las dependencias externas.

¿Por qué necesito burlarse?

Seamos honestos. Las aplicaciones complejas no se crean desde cero: utilizan bibliotecas, API y proyectos o servicios centrales que otra persona crea y mantiene. Como desarrolladores de Spring, aprovechamos la funcionalidad existente tanto como sea posible para que podamos dedicar nuestro tiempo y esfuerzo a lo que nos importa: la lógica comercial de nuestra aplicación. Dejamos los detalles a las bibliotecas, por lo que nuestras aplicaciones tienen muchas dependencias, que se muestran en naranja a continuación:

Gráfico que muestra las múltiples dependencias de un servicio de Spring. Del controlador al servicio, luego a una base de datos o bibliotecas.
Un servicio Spring con múltiples dependencias

Entonces, ¿cómo enfoco las pruebas unitarias en mi aplicación (controlador y servicio) si la mayor parte de su funcionalidad depende del comportamiento de estas dependencias? ¿No estoy, al final, siempre realizando pruebas de integración en lugar de pruebas unitarias? ¿Qué pasa si necesito un mejor control sobre el comportamiento de esas dependencias o si las dependencias no están disponibles durante las pruebas unitarias?

Los beneficios de burlarse

Lo que necesito es una manera de aislar mi aplicación de esas dependencias, para poder centrar mis pruebas unitarias en el código de mi aplicación. En algunos casos, podríamos crear versiones especializadas de "prueba" de estas dependencias. Sin embargo, el uso de una biblioteca estandarizada como Mockito brinda beneficios sobre este enfoque por varias razones:

  • No es necesario que escriba y mantenga el código especial de "prueba" usted mismo.
  • Las bibliotecas simuladas pueden rastrear invocaciones contra simulacros, proporcionando una capa adicional de validación.
  • Las bibliotecas estándar como Mockito brindan funcionalidad adicional, como burlarse de métodos estáticos, métodos privados o constructores.
  • El conocimiento de una biblioteca simulada como Mockito se puede reutilizar en todos los proyectos, mientras que el conocimiento del código de prueba personalizado no se puede reutilizar.
Un gráfico que muestra cómo un servicio simulado puede reemplazar múltiples dependencias. El controlador entra en servicio o en un servicio simulado. El servicio también se conecta a una base de datos y bibliotecas, mientras que el servicio simulado no lo hace.
Un servicio simulado reemplaza múltiples dependencias

Dependencias en Spring

En general, las aplicaciones Spring dividen la funcionalidad en Beans. Un controlador puede depender de un Service Bean y el Service Bean puede depender de un EntityManager, una conexión JDBC u otro Bean. La mayoría de las veces, las dependencias de las que debe aislarse el código bajo prueba son beans. En una prueba de integración, tiene sentido que todas las capas sean reales. Pero para las pruebas unitarias, debemos decidir qué dependencias deben ser reales y cuáles deben ser simuladas.

Spring permite a los desarrolladores definir y configurar beans utilizando XML, Java o una combinación de ambos para proporcionar una mezcla de beans simulados y reales en su configuración. Dado que los objetos simulados deben definirse en Java, se debe utilizar una clase de configuración para definir y configurar los beans simulados.

¿Cómo empiezo a burlarme de las dependencias en una prueba de primavera?

Una herramienta de prueba automatizada, como Unit Test Assistant (UTA) de Parasoft Jtest, puede ayudarlo a crear pruebas unitarias significativas que prueben la funcionalidad de sus aplicaciones Spring. Cuando UTA genera una prueba Spring, todas las dependencias de su controlador se configuran como simulacros para que cada prueba obtenga el control de la dependencia. Cuando se ejecuta la prueba, UTA detecta las llamadas a métodos realizadas en un objeto simulado para métodos que aún no tienen configurado el método simulado y recomienda que esos métodos se simulen. Luego podemos usar una solución rápida para simular automáticamente cada método.

Aquí hay un controlador de ejemplo que depende de un PersonaServicio:

@Controller
@RequestMapping("/people")
public class PeopleController {
 
    @Autowired
    protected PersonService personService;

    @GetMapping
    public ModelAndView people(Model model){
   
        for (Person person : personService.getAllPeople()) {
            model.addAttribute(person.getName(), person.getAge());
        }
        return new ModelAndView("people.jsp", model.asMap());
    }
}

Y una prueba de ejemplo, generada por el asistente de pruebas unitarias de Parasoft Jtest:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class PeopleControllerTest {
 
    @Autowired
    PersonService personService;

    MockMvc mockMvc;
 
    // Other fields and setup
 
    @Configuration
    static class Config {
 
        // Other beans
 
        @Bean
        public PersonService getPersonService() {
            return mock(PersonService.class);
        }
    }
 
    @Test
    public void testPeople() throws Exception {
        // When
        ResultActions actions = mockMvc.perform(get("/people"));
    }
}

Aquí, la prueba usa una clase interna anotada con @Configuración, que proporciona dependencias de beans para el controlador bajo prueba utilizando la configuración de Java. Esto nos permite burlarnos del PersonaServicio en el método del frijol. Todavía no hay métodos simulados, por lo que cuando ejecuto la prueba, veo la siguiente recomendación:

Esto significa que el getAllPeople () se invocó el método en mi burla PersonaServicio, pero la prueba aún no configura la simulación para este método. Cuando elijo la opción de solución rápida "Simule", la prueba se actualiza:

@Test
public void testPeople() throws Exception {
 Collection<Person> getAllPeopleResult = new ArrayList<Person>();
 doReturn(getAllPeopleResult).when(personService).getAllPeople();
 // When
 ResultActions actions = mockMvc.perform(get("/people"));

Cuando vuelvo a ejecutar la prueba, pasa. Todavía debería poblar el Colecciones que es devuelto por getAllPeople (), pero el desafío de configurar mis dependencias simuladas está resuelto.

Tenga en cuenta que podría mover el método generado de burla del método de prueba al método de frijol de la clase de configuración. Si hago esto, significa que cada prueba en la clase simulará el mismo método de la misma manera. Dejar el método burlándose en el método de prueba significa que el método puede ser burlado de manera diferente entre diferentes pruebas.

¿Cómo simular dependencias en Spring Boot?

Spring Boot hace que burlarse de los frijoles sea aún más fácil. En lugar de usar un @autocableado campo para el bean en la prueba y una clase de configuración que lo define, simplemente puede usar un campo para el bean y anotarlo con @MockBean. Spring Boot creará una simulación para el bean utilizando el marco de simulación que encuentra en el classpath y lo inyectará de la misma manera que se puede inyectar cualquier otro bean en el contenedor.

Al generar pruebas de Spring Boot con Unit Test Assistant, el @MockBean se utiliza la funcionalidad en lugar de la clase de configuración.

@SpringBootTest
@AutoConfigureMockMvc
public class PeopleControllerTest {
    // Other fields and setup – no Configuration class needed!

    @MockBean
    PersonService personService;

    @Test
    public void testPeople() throws Exception {
        ...
    }
}

Configuración de XML frente a Java

En el primer ejemplo anterior, la clase Configuration proporcionó todos los beans al contenedor Spring. Como alternativa, puede utilizar la configuración XML para la prueba en lugar de la clase Configuración; o puedes combinar los dos. Por ejemplo:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:/**/testContext.xml" })
public class PeopleControllerTest {
 
    @Autowired
    PersonService personService;
 
    // Other fields and setup
 
    @Configuration
    static class Config {
        @Bean
        @Primary
        public PersonService getPersonService() {
            return mock(PersonService.class);
        }
    }
 
    // Tests
}

Aquí, la clase hace referencia a un archivo de configuración XML en el @ContextConfiguración anotación (no se muestra aquí) para proporcionar la mayoría de los beans, que podrían ser beans reales o beans específicos de la prueba. También proporcionamos un @Configuración clase, donde PersonaServicio se burla. los @Primario anotación indica que incluso si una PersonaServicio bean se encuentra en la configuración XML, esta prueba utilizará el bean simulado de la @Configuración clase en su lugar. Este tipo de configuración puede hacer que el código de prueba sea más pequeño y más fácil de administrar.

Puede configurar UTA para generar pruebas utilizando cualquier @ContextConfiguración atributos que necesita.

Burlarse de métodos estáticos

A veces, se accede a las dependencias de forma estática. Por ejemplo, una aplicación podría acceder a un servicio de terceros a través de una llamada de método estático:

public class ExternalPersonService {
    public static Person getPerson(int id) {
       RestTemplate restTemplate = new RestTemplate();
       try {
           return restTemplate.getForObject("http://domain.com/people/" + id, Person.class);
        } catch (RestClientException e) {
            return null;
        }
    }
}

En nuestro controlador:

    @GetMapping
    public ResponseEntity&amp;lt;Person&amp;gt; getPerson(@PathVariable("id") int id, Model model)
    {
        Person person = ExternalPersonService.getPerson(id);
        if (person != null) {
            return new ResponseEntity&amp;lt;Person&amp;gt;(person, HttpStatus.OK);
        }
        return new ResponseEntity&amp;lt;&amp;gt;(HttpStatus.NOT_FOUND);
    }

En este ejemplo, nuestro método controlador utiliza una llamada de método estático para obtener un objeto Person de un servicio de terceros. Cuando creamos una prueba JUnit para este método de controlador, se realizará una llamada HTTP real al servicio cada vez que se ejecute la prueba.

En cambio, burlémonos de la estática ExternalPersonService.getPerson () método. Esto evita la llamada HTTP y nos permite proporcionar una Persona respuesta de objeto que se adapte a nuestras necesidades de prueba. Unit Test Assistant puede facilitar la simulación de métodos estáticos con Mockito.

UTA genera una prueba para el método del controlador anterior que se ve así:

@Test
public void testGetPerson() throws Throwable {
    // When
    Int id = 1L;
    ResultActions actions = mockMvc.perform(get("/people/" + id));

    // Then
    actions.andExpect(status().isOk());
}

Cuando ejecutamos la prueba, veremos que se realiza la llamada HTTP en el árbol de flujo de UTA. Busquemos la llamada a ExternalPersonService.getPerson () y burlarse de él en su lugar:

Captura de pantalla que muestra el árbol de flujo del asistente de pruebas unitarias de Parasoft Jtest

La prueba se actualiza para simular el método estático para esta prueba usando Mockito:

@Test
public void testGetPerson() throws Throwable {
    MockedStatic<ExternalPersonService>mocked = mockStatic(ExternalPersonService.class);
    mocks.add(mocked);
 
    Person getPersonResult = null; // UTA: default value
    mocked.when(()->ExternalPersonService.getPerson(anyInt())).thenReturn(getPersonResult);
 
    // When
    int id = 1;
    ResultActions actions = mockMvc.perform(get("/people/" + id));

    // Then
    actions.andExpect(status().isOk());

    }
 
    Set<AutoCloseable> mocks = new HashSet&amp;lt;&amp;gt;();
 
    @After
    public void closeMocks() throws Throwable {
        for (AutoCloseable mocked : mocks) {
            mocked.close();
        }
    }

El Mockito MockStatic El método crea un simulacro estático para la clase, a través del cual se pueden configurar llamadas estáticas específicas. Para asegurarse de que esta prueba no afecte a otras en la misma ejecución, los objetos MockedStatic deben cerrarse cuando finaliza la prueba, por lo que todos los simulacros se cierran en el cerrarMocks() método que se agrega a la clase de prueba.

Usando UTA, ahora podemos seleccionar el obtenerResultadoPersona variable e instanciarla, de modo que la llamada al método simulado no regrese nulo:

    String name="";//UTA:default value
    int age=0;//UTA:default value
    Person getPersonResult=newPerson(name, age);

Cuando volvamos a ejecutar la prueba, obtenerResultadoPersona se devuelve de la burlaExternalPersonService.getPerson () método y la prueba pasa.

Nota: Desde el árbol de flujo, también puede elegir "Agregar patrón de método simulable" para las llamadas a métodos estáticos. Esto configura Unit Test Assistant para simular siempre esas llamadas a métodos estáticos al generar nuevas pruebas.

Conclusión

Las aplicaciones complejas a menudo tienen dependencias funcionales que complican y limitan la capacidad de un desarrollador para realizar pruebas unitarias de su código. El uso de un marco de simulación como Mockito puede ayudar a los desarrolladores a aislar el código que se está probando de estas dependencias, lo que les permite escribir mejores pruebas unitarias con mayor rapidez. Solución de productividad para desarrolladores de Java de Parasoft facilita la gestión de dependencias configurando nuevas pruebas para usar simulacros, y encontrando simulacros de métodos faltantes en tiempo de ejecución y ayudando a los desarrolladores a generar simulacros para ellos.

Mejore las pruebas unitarias para Java con automatización: mejores prácticas para desarrolladores de Java