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


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

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.

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).

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

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

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.

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:

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:

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:

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:

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.

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.

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.

Dejar un Comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *