A logo showing the text blog.marcnuri.com
English
Inicio»Java»MockMvc – Introducción a Spring MVC testing framework: Probando endpoints

Entradas Recientes

  • Fabric8 Kubernetes Client 7.2.0 está disponible!
  • Conectarse a un servidor MCP con JavaScript y AI SDK
  • Conectarse a un servidor MCP con JavaScript y LangChain.js
  • El Futuro de las Herramientas para Desarrolladores en la era de la IA
  • Conectarse a un servidor Model Context Protocol (MCP) con Java y LangChain4j

Categorías

  • Antiguo
  • Front-end
  • Go
  • Herramientas
  • Industria y negocios
  • Inteligencia Artificial
  • Java
  • JavaScript
  • Operaciones
  • Personal
  • Proyectos personales

Archivos

  • mayo 2025
  • abril 2025
  • marzo 2025
  • febrero 2025
  • enero 2025
  • diciembre 2024
  • noviembre 2024
  • agosto 2024
  • junio 2024
  • mayo 2024
  • abril 2024
  • marzo 2024
  • febrero 2024
  • enero 2024
  • diciembre 2023
  • noviembre 2023
  • octubre 2023
  • septiembre 2023
  • agosto 2023
  • julio 2023
  • junio 2023
  • mayo 2023
  • abril 2023
  • marzo 2023
  • febrero 2023
  • enero 2023
  • diciembre 2022
  • noviembre 2022
  • octubre 2022
  • agosto 2022
  • julio 2022
  • mayo 2022
  • marzo 2022
  • febrero 2022
  • enero 2022
  • diciembre 2021
  • noviembre 2021
  • octubre 2021
  • septiembre 2021
  • agosto 2021
  • julio 2021
  • diciembre 2020
  • octubre 2020
  • agosto 2020
  • junio 2020
  • mayo 2020
  • marzo 2020
  • febrero 2020
  • enero 2020
  • noviembre 2019
  • octubre 2019
  • julio 2019
  • diciembre 2018
  • agosto 2018
  • julio 2018
  • junio 2018
  • mayo 2018
  • marzo 2018
  • febrero 2018
  • noviembre 2017
  • octubre 2017
  • agosto 2017
  • julio 2017
  • enero 2017
  • julio 2016
  • enero 2016
  • diciembre 2015
  • noviembre 2015
  • diciembre 2014
  • marzo 2014
  • febrero 2011
  • junio 2008
  • mayo 2008
  • abril 2008
  • enero 2008
  • junio 2007
  • mayo 2007
  • abril 2007
  • marzo 2007

MockMvc – Introducción a Spring MVC testing framework: Probando endpoints

2018-08-01 en Java etiquetado Agile / Automatización / JUnit / Mockito / MockMVC / REST / Spring Framework / Spring Boot / Testing por Marc Nuri | Última actualización: 2021-04-05
English version

Introducción

En esta publicación veremos cómo utilizar MockMvc para probar endpoints creados con Spring. Este es el primer post de una serie dónde analizaremos las principales ventajas de utilizar Spring MVC test framework para probar clases de tipo Controller respecto a otras soluciones y cuáles son las distintas formas de configurar los tests.

El código fuente completo del artículo puede encontrarse en GitHub. El proyecto contiene un controller MVC estándar que reenvía las llamadas a un recurso estático y dos controladores REST que devuelven una lista de lenguajes y otra de cafés.

MockMvc y la pirámide de los tests

Test Pyramid
Test Pyramid

En su libro, Succeeding with Agile: Software Development Using Scrum, Mike Cohn describe la pirámide de automatización de los tests como una estrategia de automatización de tests en términos de cantidad y esfuerzo que deben dedicarse a cada uno de los tipos de test. La pirámide también la emplean otros autores para definir un portfolio de tests en base al coste y velocidad. La base de la pirámide son los tests unitarios, por lo que debemos entender que los tests unitarios son los cimientos de la estrategia de testing y que debería de haber muchos más tests unitarios que tests end-to-end de alto nivel.

Como ya sabrás, los test unitarios se centran en probar el componente o unidad más pequeño de un software. En la programación orientada a objetos esto se traduce normalmente un una sola clase o interfaz, o un método o función dentro de esta clase. El test debe de realizarse de forma aislada a otras unidades del mismo software, para conseguir esto normalmente las dependencias de la unidad a probar se simulan (mock). Una de las características principales de los tests unitarios es que su ejecución debe de ser realmente rápida.

De forma similar, los tests de integración se centran en probar una combinación de las unidades o componentes anteriormente mencionados. Estrictamente hablando, los tests utilizando MockMvc se encuentran en la frontera entre los tests unitarios y lost tests de integración. No son tests unitarios porque los endpoints se prueban de forma integrada con un contenedor MVC con dependencias e inputs simulados. Pero tampoco podríamos considerar estos test como de integración, ya que si se definen correctamente, con MockMvc sólo probaremos un único componente con una plataforma simulada, pero no una combinación de componentes.

Tecnicismos a un lado, cuando preparemos nuestro portfolio de tests para un proyecto, los tests empleando Spring MVC deberían de tratarse como tests unitarios y ser parte de los cimientos de nuestra estrategia de testing.

Controller de lenguajes

Para ilustrar los ejemplos de este tutorial vamos a crear un controller sencillo que devuelve una lista de lenguajes de programación.

1@Controller
2@RequestMapping(value = "/api", produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
3public class LanguageApiController {
4
5  private final LanguageService languageService;
6
7  @Autowired
8  public LanguageApiController(
9      LanguageService languageService) {
10    this.languageService = languageService;
11  }
12
13  @RequestMapping(value = "/languages", method = GET)
14  public ResponseEntity<List<Language>> getLanguages(@RequestParam(value = "contains", required = false) String contains) {
15    return ResponseEntity.ok(languageService.getLanguages(contains));
16  }
17
18  @RequestMapping(value = "/languages/{name}", method = GET)
19  public ResponseEntity<Language> getLanguage(@PathVariable("name") String name) {
20    return ResponseEntity.ok(languageService.getLanguage(name).orElseThrow(() -> new SpringMockMvcException(
21        HttpStatus.NOT_FOUND, "Language was not found")));
22  }
23
24  @ExceptionHandler(SpringMockMvcException.class)
25  public ResponseEntity<String> onSpringMockMvcException(HttpServletRequest request, SpringMockMvcException ex) {
26    return ResponseEntity.status(ex.getHttpStatus()).body(String.format("%s - %s",
27        ex.getHttpStatus().value(), ex.getMessage()));
28  }
29
30}

El controlador tiene una anotación global @RequestMapping para especificar el mime-type para la salida de los endpoints declarados en la clase como application/json. La clase expone dos endpoints que se apoyan en LanguageService para realizar las operaciones de negocio.

El primero de los endpoints (/api/languages) una lista de todos los lenguajes y opcionalmente podrá ser filtrada con un parámetro (contains).

Una imagen mostrando el resultado de ejecutar una petición con curl al endpoint api/languages filtrado por el parámetro contains
Una imagen mostrando el resultado de ejecutar una petición con curl al endpoint api/languages filtrado por el parámetro contains

El siguiente endpoint (/api/language/{name}) devolverá un único lenguaje a partir del parámetro proporcionado correspondiente a su nombre (único).

Una imagen mostrando el resultado de ejecutar una petición con curl al endpoint api/languages/java
Una imagen mostrando el resultado de ejecutar una petición con curl al endpoint api/languages/java

Por último, hemos declarado un ExceptionHandler que se encargará de convertir cualquier excepción controlada SpringMockMvcException en un ResponseEntity.

Una imagen mostrando el resultado de ejecutar una petición con curl al endpoint api/languages/Gibberrish que ha sido intervenida por el ExceptionHandler
Una imagen mostrando el resultado de ejecutar una petición con curl al endpoint api/languages/Gibberrish que ha sido intervenida por el ExceptionHandler

Spring MVC test framework: ventajas

Los tests cuya ejecución depende de MockMvc se encuentran en las fronteras entre los tests de integración y los tests unitarios, tal como se ha explicado en los párrafos anteriores. A continuación se muestra un test unitario estricto donde se prueba LanguageApiController.

1@Test
2  public void getLanguages_null_shouldReturnListOfStrings() throws Exception {
3    // Given
4    final String mockedEsoteric = "Arnoldc";
5    final List<String> mockedLanguages = Stream.concat(
6        LanguageRepository.LANGUAGES.stream(),
7        Stream.of(mockedEsoteric)).collect(Collectors.toList());
8    doReturn(mockedLanguages).when(languageService).getLanguages(null);
9
10    // When
11    final ResponseEntity<List<String>> result = languageApiController.getLanguages(null);
12
13    // Then
14    assertThat(result.getBody(), hasSize(mockedLanguages.size()));
15    assertThat(result.getBody(), hasItem(mockedEsoteric));
16  }
17}

Este test comprueba que el método devuelve la lista esperada cuando se le pasa null como argumento. Esta prueba nos aportará un 100% de cobertura de test para este código, sin embargo, hay muchos aspectos de la especificación que no se comprueban, de forma más específica:

  • Cabeceras de la negociación de contenidos (content negotiation headers): Este controlador se ha diseñado para producir únicamente contenido en formato application/json.
  • Códigos de respuesta (response codes): Con MockMvc somos capaces que el código de respuesta se corresponde con el esperado (200, 201…), incluso cuando el código se asigna fuera del método del controlador o post-procesado por una anotación/aspect.
  • Serialización/Deserialización de objetos a JSON: Con MockMvc podemos comprobar que el JSON que pasamos cuando hacemos una petición con contenido se deserializa correctamente y que el JSON generado en lel cuerpo de la respuesta tiene la estructura y contenido esperados.
  • Disponibilidad de cabeceras en la respuesta.
  • Muchos otros escenarios y configuraciones que van más allá de la implementación concreta del método pero que son parte de la especificación e implementación del endpoint (Validación, Seguridad, Gestión de excepciones…)

Anotación WebMvcTest

La forma más sencilla de ejecutar un test con MockMvc es utilizar la anotación de Spring Boot @WebMvcTest. Está anotación configurará la suite JUnit con SpringRunner para que despliegue una aplicación web de forma parcial auto configurando sólo aquellos componentes necesarios para una aplicación web MVC.

En mi opinión, esta no es la mejor estrategia para probar una aplicación, ya que se desplegará una aplicación completa simulada para cada suite de tests. Esto puede ser bastante exagerado cuando lo único que queremos es comprobar unitariamente el controlador y no su integración con el resto de componentes de la aplicación (pirámide de los tests).

El siguiente test empleará @WebMvcTest para autoconfigurar el entorno de test y lanzar los test de comprobación utilizando una instancia inyectada de MockMvc:

1@RunWith(SpringRunner.class)
2@WebMvcTest(controllers = LanguageApiController.class)
3public class LanguageApiControllerWebMvcTest {
4
5  @Autowired
6  private MockMvc mockMvc;
7
8  @Test
9  public void getLanguages_null_shouldReturnOk() throws Exception {
10    // Given
11    // Real application context
12
13    // When
14    final ResultActions result = mockMvc.perform(
15        get("/api/languages")
16            .accept(MimeTypeUtils.APPLICATION_JSON_VALUE));
17
18    // Then
19    final int expectedSize = LANGUAGES.size();
20    final String[] expectedLanguageNames = LANGUAGES.stream().map(Language::getName)
21        .collect(Collectors.toList()).toArray(new String[LANGUAGES.size()]);
22    result.andExpect(status().isOk());
23    result.andExpect(jsonPath("$.length()").value(expectedSize));
24    result.andExpect(jsonPath("$[*].name", containsInAnyOrder(expectedLanguageNames)));
25  }
26}

Insistir en que esta no es la mejor forma de preparar una prueba si estamos siguiendo la estrategia de la pirámide de los test. Otras alternativas para crear instancias de MockMvc programáticamente se describen en los siguientes escenarios.

Configuración de integración con un contexto de aplicación web (Web Application Context)

Hay diversas formas en las que MockMvc puede instanciarse en un test de forma programática, la forma en la que esto se haga definirá el grado de integración que el test realizará sobre los componentes de la aplicación. El mayor nivel de integración lo conseguiremos instanciando MockMvc empleando .webAppContextSetup(...). En mi opinión este no es el mejor método para probar un endpoint ya que muchas de las ventajas de las pruebas unitarias se pierden (los test son más lentos, se incrementa la complejidad a la hora de simular objetos…).

El siguiente test verifica el comportamiento de la aplicación empleando esta técnica:

1@RunWith(SpringJUnit4ClassRunner.class)
2@WebAppConfiguration
3@ContextConfiguration(classes = {
4    SpringMockMvcConfiguration.class
5})
6public class LanguageApiControllerApplicationIntegrationTest {
7
8  @Autowired
9  private WebApplicationContext webApplicationContext;
10
11  private MockMvc mockMvc;
12
13  @Before
14  public void setUp() {
15    mockMvc = MockMvcBuilders
16        .webAppContextSetup(webApplicationContext)
17        .build();
18  }
19/*...*/
20  @Test
21  public void getLanguages_null_shouldReturnOk() throws Exception {
22    // Given
23    // Real application context
24
25    // When
26    final ResultActions result = mockMvc.perform(
27        get("/api/languages")
28            .accept(MimeTypeUtils.APPLICATION_JSON_VALUE));
29
30    // Then
31    final int expectedSize = LANGUAGES.size();
32    final String[] expectedLanguageNames = LANGUAGES.stream().map(Language::getName)
33        .collect(Collectors.toList()).toArray(new String[LANGUAGES.size()]);
34    result.andExpect(status().isOk());
35    result.andExpect(jsonPath("$.length()").value(expectedSize));
36    result.andExpect(jsonPath("$[*].name", containsInAnyOrder(expectedLanguageNames)));
37  }
38
39}

Hay dos secciones importantes en el código anterior. Entre las anotaciones de la clase encontramos @WebAppConfiguration que indica que un WebApplicationContext debería de cargarse para el test empleando el path por defecto a la raíz de la aplicación web. Además se define un @ContextConfiguration para indicar qué clases/beans deberían de cargarse para el test. En este caso, vamos a reutilizar el Bean de configuración empleado en la aplicación real, de este modo podremos comprobar la integración completa de todos los componentes de la aplicación.

En la siguiente sección del código encontramos la puesta en marcha del test setUp/@Before. En este caso vamos a construir una instancia de MockMvc empleando un WebApplicationContext completamente inicializado. Spring nos permite inyectar este contexto mediante @Autowire debido a que estamos utilizando la anotación @WebAppConfiguration a nivel de clase.

El test comprueba que el endpoint se expone en la ruta (path) esperada y que responde con los lenguajes existentes en el repositorio real de la aplicación cuando recibe una petición con las cabeceras (headers) apropiadas. De este modo, además de verificar que el controller se ha implementado de forma adecuada, también validamos que la configuración en base a aspectos (i.e. @RequestMapping…) se comporta de la forma esperada.

Tal como ya se ha mencionado, este test comparte la configuración real de la aplicación WebMvc (SpringMockMvcConfiguration), por tanto durante el test habrá disponible un application context completo. Todo ello implica que el test tardará más ejecutarse y consumirá bastantes recursos, además de posiblemente necesidad de acceso a bases de datos y otros recursos reales. Para restringir el número de Beans en el test, debería de haber una o más configuraciones específicas de aplicación WebMvc para las pruebas.

Este tipo de configuración sólo debería de valorarse cuando lo que queremos es realizar pruebas con un grado considerable de integración entre los distintos componentes. En la siguiente sección se describe la configuración independiente (standalone) de MockMvc, esta debería de ser la configuración de referencia cuando los tests se centran únicamente en verificar el funcionamiento de los controladores (controllers).

Configuración MockMvc standalone

MockMvcBuilders ofrece una forma adicional para configurar MockMvc, standalonSetup(...). Este método nos permite registrar uno o más Controllers sin necesidad de desplegar un WebApplicationContext completo. Simular las dependencias de los controladores empleando esta técnica es muy sencillo y nos permite probar un controlador aislado de sus dependencias, del mismo modo que lo hemos hecho en el ejemplo con el test unitario estricto. Por el contrario, con esta técnica también somos capaces de verificar la ruta (path), método de la petición (request method), cabeceras (headers), respuestas, etc. por lo que tenemos lo mejor de ambos mundos.

El siguiente test demuestra este comportamiento:

1@RunWith(SpringJUnit4ClassRunner.class)
2@ContextConfiguration(classes = {
3    LanguageApiController.class
4})
5@Import(Config.class)
6public class LanguageApiControllerTest {
7
8  @Autowired
9  private LanguageService languageService;
10
11  @Autowired
12  private LanguageApiController languageApiController;
13
14  private MockMvc mockMvc;
15
16  @Before
17  public void setUp() {
18    mockMvc = MockMvcBuilders
19        .standaloneSetup(languageApiController)
20        .build();
21  }
22
23/* ... */
24
25  @TestConfiguration
26  protected static class Config {
27
28    @Bean
29    public LanguageService languageService() {
30      return Mockito.mock(LanguageService.class);
31    }
32
33  }
34
35}

Esta configuración difiere un poco de la del anterior ejemplo. La anotación @WebAppConfiguration ya no es necesaria, por lo que Spring ya no cargará un contexto de aplicación completo (menor sobrecarga y uso de recursos). La anotación @ContextConfiguration incluye una referencia al controlador que queremos probar, Spring creará un Bean para este Controller y lo inyectará en el test (@Autowire). Por último hay una configuración interna donde declaramos los mock Beans para las dependencias del controlador. Spring inyectará estas instancias simuladas dentro del controlador. Los mock beans también podrán inyectarse dentro del test, por lo que preparar datos simulados será realmente fácil.

Languages Happy Path

El siguiente test emplea la configuración standalone de MockMvc y describe el happy path para el endpoint /api/languages:

1@Test
2public void getLanguages_null_shouldReturnOk() throws Exception {
3  // Given
4  final Language mockedEsoteric = new Language("Arnoldc", "Lauri Hartikka");
5  final List<Language> mockedLanguages = Stream.concat(
6      LanguageRepository.LANGUAGES.stream(),
7      Stream.of(mockedEsoteric)).collect(Collectors.toList());
8  doReturn(mockedLanguages).when(languageService).getLanguages(null);
9
10  // When
11  final ResultActions result = mockMvc.perform(
12      get("/api/languages")
13          .accept(MimeTypeUtils.APPLICATION_JSON_VALUE));
14
15  // Then
16  result.andExpect(status().isOk());
17  result.andExpect(jsonPath("$.length()").value(mockedLanguages.size()));
18  result.andExpect(jsonPath("$[?(@.name === 'Arnoldc')]").exists());
19}

En la sección Given preparamos una lista de lenguajes de programación que serán devueltas por el Bean simulado LanguageService cuando el método getLanguages recibe un parámetro con valor null.

A continuación hacemos la llamada GET al endpoint en el entorno Mock Mvc empleando un header Accept: application/json. Finalmente, con la respuesta recibida, comprobamos que el código de estado es el esperado (200) y mediante JSON path verificamos que se incluyen todos los lenguajes que hemos simulado.

Tal como ya hemos mencionado, en este caso no sólo estamos comprobando la implementación del método en el controller, pero también su configuración a efectos de cabeceras de la negociación de contenidos, serialización del objeto en formato JSON y códigos de respuesta HTTP.

Invalid Accept header

Este test pone de manifiesto como el uso de MockMvc nos permitirá comprobar si las anotaciones de un endpoint se comportan de la forma esperada y que el endpoint únicamente queda expuesto cuando recibe en la petición una cabecera Accept: application/json.

1@Test
2public void getLanguages_invalidAcceptHeader_shouldReturnNotAcceptable() throws Exception {
3  // Given
4  final String invalidAcceptMimeType = MimeTypeUtils.APPLICATION_XML_VALUE;
5  doReturn(LanguageRepository.LANGUAGES).when(languageService).getLanguages(null);
6
7  // When
8  final ResultActions result = mockMvc.perform(
9      get("/api/languages")
10          .accept(invalidAcceptMimeType));
11
12  // Then
13  result.andExpect(status().isNotAcceptable());
14}

En este caso, la llamada a través de MockMvc se realiza utilizando una cabecera Accept: application/xml. El endpoint se ha configurado únicamente para devolver datos en formato JSON, por lo que Spring MVC nunca invocará el método del endpoint y devolverá un código de estado 406 (Not Acceptable) para la respuesta.

Probando el ExceptionHandler

El LanguageApiControllerincluye un @ExceptionHandler que se encarga de gestionar las excepciones controladas que se lanzan desde cualquier método del Controller. Cuando invocamos el endpoint /api/languages/{name}, si no se encuentra ningún lenguaje cuyo nombre sea igual al de la variable del path, un excepción controlada SpringMockMvc se lanza desde el LanguageService.

El siguiente test verifica que el @ExceptionHandler se comporta de la forma esperada y que la excepción controlada se serializa mediante un ResponseEntity con el código de respuesta y contenido esperado.

1@Test
2public void getLanguage_nonExistingLanguage_shouldReturnNotFound() throws Exception {
3  // Given
4  final String mockedLanguageName = "Arnoldc";
5  doReturn(Optional.empty()).when(languageService).getLanguage(mockedLanguageName);
6
7  // When
8  final ResultActions result = mockMvc.perform(
9      get("/api/languages/".concat(mockedLanguageName))
10          .accept(MimeTypeUtils.APPLICATION_JSON_VALUE));
11
12  // Then
13  result.andExpect(status().isNotFound());
14}

En la sección Given configuramos un LanguageService simulado para que devuelva un Optional vacío cuando se le hace una petición de lenguaje con el nombre “Arnoldc”.

A continuación, se realiza una petición GET al endpoint /api/languages/Arnoldc. Por último se comprueba que el código de respuesta es 404 (Not Found) de acuerdo con el código del @ExceptionHandler ...ResponseEntity.status(ex.getHttpStatus())....

Conclusión

Esta publicación es una introducción a Spring MVC testing framework. Se resaltan las ventajas de utilizar MockMvc en lugar de hacer test unitarios estándar. Se muestran tres formas para preparar MockMvc y como el empleo de standaloneSetup nos permite obtener las ventajas de los test unitarios y de los de integración. El post se centra únicamente en probar un controller de Spring REST aunque MockMvc se puede utilizar para probar la suite completa del framework Spring MVC.

El código fuente completo de este artículo puede encontrarse en Github.

Twitter iconFacebook iconLinkedIn iconPinterest iconEmail icon

Comentarios en "MockMvc – Introducción a Spring MVC testing framework: Probando endpoints"

  • Avatar for jose
    jose
    2020-12-22 11:59
    Buenisimo

Navegador de artículos
Isotope Mail Client: Introducción y funcionalidadesReact : Babel + Webpack + Sass aplicación básica
© 2007 - 2025 Marc Nuri