Cómo testear un controller en Spring Boot – una guía práctica
Cómo testear controllers de Spring Boot: una guía práctica con ejemplos de unit tests e integration tests con y sin MockMvc.
Existen diferentes formas de testear tus clases Controller (capa Web o API) en Spring Boot; algunas proporcionan soporte para escribir Unit Tests puros, y otras son más útiles para Integration Tests. En este post, cubriré los tres enfoques principales de testing disponibles para Spring: MockMVC en modo standalone, MockMVC junto con SpringRunner, y SpringBootTest.
ACTUALIZADO en Mayo 2025. Los ejemplos de código usan Java 24, Spring Boot 3.4, y JUnit 5. Si tienes curiosidad sobre las últimas actualizaciones, revisa el Pull Request para ver los cambios.
Introducción
Spring Boot ofrece varios enfoques diferentes para testing. Es un framework en constante evolución, por lo que las nuevas versiones ofrecen más opciones mientras mantienen las antiguas para compatibilidad hacia atrás. Esto resulta en múltiples formas de testear la misma parte de nuestro código y cierta confusión sobre cuándo usar qué. En este post, te ayudaré a entender las diferentes alternativas, por qué están disponibles, y cuándo usar cada una.
Este artículo se enfoca en el testing de Controllers ya que es la parte más confusa, donde mockear objetos es posible en diferentes niveles.
La aplicación de ejemplo
Usaremos código de ejemplo a lo largo de este post para poner en práctica los diferentes conceptos cubiertos.
Todo el código en este post está disponible en GitHub: Spring Boot Testing Strategies. Si lo encuentras útil, ¡por favor dale una estrella!
En resumen, es solo un repositorio de entidades -superhéroes- expuesto a través de una REST API. Es esencial listar algunas particularidades de la aplicación para entender más adelante qué sucede al usar las diferentes estrategias:
- Si un superhéroe no puede encontrarse por su identificador, se lanza una
NonExistingHeroException. Un@RestControllerAdvicede Spring intercepta esa excepción y la transforma en un código de estado 404 – NOT_FOUND. - Una clase
SuperHeroFilteragrega un header a la respuesta HTTP:X-SUPERHERO-APP.
Tests del lado del Servidor y del Cliente
Para empezar, podemos separar tests del lado del servidor y del lado del cliente.
Los tests del lado del servidor son la forma más extendida de testing: realizas tu petición, y quieres verificar cómo se comporta el servidor, la composición de la respuesta, el contenido de la respuesta, etc.
Los tests del lado del cliente no son tan estándar, pero son útiles cuando verificas la composición y acciones de una petición. En estos tests, mockeas el comportamiento del servidor y luego llamas a algún código (de tu lado) que indirectamente realizará una petición a ese servidor. Eso es precisamente lo que quieres testear; quieres verificar que hubo una petición y el contenido de esa petición. No te importa el contenido de la respuesta (mockeaste esa parte). Desafortunadamente, no hay muchos buenos ejemplos de esto. No son muy útiles, incluso si revisas los ejemplos oficiales (ve los comentarios del Javadoc). De todos modos, la idea esencial aquí es que pueden usarse cuando estás escribiendo una aplicación cliente y quieres verificar las peticiones desde tu lado hacia el mundo exterior.
Nos enfocaremos en tests del lado del servidor, que verifican cómo funciona la lógica del servidor. En este caso, usualmente mockeas las peticiones, y quieres verificar cómo reacciona tu lógica del servidor. Este tipo de test está estrechamente relacionado con la capa Controller en tu aplicación ya que es la parte de Spring que maneja las peticiones HTTP.
Tests del Lado del Servidor
Si hacemos zoom en los tests del lado del servidor, podemos identificar dos estrategias principales en Spring: escribir tests de Controller usando el enfoque MockMVC o usando RestTemplate. Deberías favorecer la primera estrategia (MockMVC) si quieres codificar un Unit Test real, mientras que deberías usar RestTemplate si pretendes escribir un Integration Test. La razón es que, con MockMVC, podemos afinar nuestras aserciones para el Controller. RestTemplate, por otro lado, usará el WebApplicationContext de Spring (parcial o completamente, dependiendo de si está usando el modo Standalone o no). Expliquemos estas dos estrategias con más detalle.
Tests Dentro del Servidor
Podemos testear directamente la lógica de nuestro controller sin necesitar un servidor web. Eso es lo que llamo testing dentro del servidor, y está más cerca de la definición de un Unit Test. Para hacerlo posible, debes mockear todo el comportamiento del servidor web, así que de alguna manera, te estás perdiendo partes a testear en nuestra aplicación. No te preocupes, porque un Integration Test puede cubrir perfectamente esas partes.
Estrategia 1: Ejemplo de Spring MockMVC en Modo Standalone
En Spring, puedes escribir un test dentro del servidor usando MockMVC en modo standalone, así que no estás cargando un contexto de Spring. Veamos un ejemplo de esto.
Ejemplo de código MockMVC standalone
Enfoque MockMVC Standalone – SuperHeroController
@ExtendWith(MockitoExtension.class)
public class SuperHeroControllerMockMvcStandaloneTest {
private MockMvc mvc;
@Mock
private SuperHeroRepository superHeroRepository;
@InjectMocks
private SuperHeroController superHeroController;
// This object will be magically initialized by the initFields method below.
private JacksonTester<SuperHero> jsonSuperHero;
@BeforeEach
public void setup() {
// We would need this line if we would not use the MockitoExtension
// MockitoAnnotations.initMocks(this);
// Here we can't use @AutoConfigureJsonTesters because there isn't a Spring context
JacksonTester.initFields(this, new ObjectMapper());
// MockMvc standalone approach
mvc = MockMvcBuilders.standaloneSetup(superHeroController)
.setControllerAdvice(new SuperHeroExceptionHandler())
.addFilters(new SuperHeroFilter())
.build();
}
@Test
public void canRetrieveByIdWhenExists() throws Exception {
// given
given(superHeroRepository.getSuperHero(2))
.willReturn(new SuperHero("Rob", "Mannon", "RobotMan"));
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes/2")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
);
}
@Test
public void canRetrieveByIdWhenDoesNotExist() throws Exception {
// given
given(superHeroRepository.getSuperHero(2))
.willThrow(new NonExistingHeroException());
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes/2")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
assertThat(response.getContentAsString()).isEmpty();
}
@Test
public void canRetrieveByNameWhenExists() throws Exception {
// given
given(superHeroRepository.getSuperHero("RobotMan"))
.willReturn(Optional.of(new SuperHero("Rob", "Mannon", "RobotMan")));
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes?name=RobotMan")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
);
}
@Test
public void canRetrieveByNameWhenDoesNotExist() throws Exception {
// given
given(superHeroRepository.getSuperHero("RobotMan"))
.willReturn(Optional.empty());
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes?name=RobotMan")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo("null");
}
@Test
public void canCreateANewSuperHero() throws Exception {
// when
MockHttpServletResponse response = mvc.perform(
post("/superheroes/")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
}
@Test
public void headerIsPresent() throws Exception {
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes/2")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getHeader("X-SUPERHERO-APP")).isEqualTo("super-header");
}
}
Ahora revisemos las partes más importantes.
MockitoExtension y MockMVC
Estamos usando la anotación MockitoExtension para JUnit 5 para habilitar el uso de mocks. (Mockito 2.x.x introdujo la capacidad de extensiones de JUnit 5, así que ya no necesitamos la llamada a initMocks()).
La instancia de MockMvc no está siendo auto-configurada, así que estamos usando la clase MockMvcBuilders para configurarla manualmente.
Inicialización de JacksonTester
El JacksonTester es una clase de utilidad que envuelve la funcionalidad de Jackson para trabajar con objetos JSON dentro de tests. Como no hay un Contexto de Spring para auto-cablearlo, necesitamos inicializarlo a través de un método estático: JacksonTester.initFields().
Configurar el Standalone Setup en MockMVC
El método standaloneSetup() nos permite registrar uno o más controllers sin la necesidad de usar el contexto completo de Spring. Esto nos da la ventaja de cargar solo las piezas que necesitamos.
Como queremos testear también el manejo de excepciones y el comportamiento del filtro, podemos agregarlos explícitamente con .setControllerAdvice(new SuperHeroExceptionHandler()) y .addFilters(new SuperHeroFilter()) respectivamente.
Testing de ControllerAdvice y Filter con MockMVC
Porque agregamos el ControllerAdvice, obtenemos un código de estado 404 real cuando se lanza la NonExistingHeroException. Lo mismo sucede con el filtro: obtenemos el header especial en la respuesta cuando realizamos nuestras peticiones.
Mejores Aserciones con BDDMockito y AssertJ
Nota que estamos usando las declaraciones given(...).willReturn() (BDDMockito) en lugar del enfoque usual when/then. La idea es que estamos usando given/when/then dentro de nuestros tests con un objetivo descriptivo. El enfoque no cambia la funcionalidad.
También estamos usando AssertJ en lugar de las aserciones clásicas de JUnit porque ofrecen una API más fluida.
Estrategia 2: Ejemplo de Spring MockMVC con WebApplicationContext
La segunda estrategia que podemos usar para escribir Unit Tests para un Controller también involucra MockMVC, pero en este caso, usamos el WebApplicationContext de Spring. Como aún estamos usando una estrategia dentro del servidor, no se despliega ningún servidor web en este caso.
Ejemplo de código MockMVC y WebMvcTest
@AutoConfigureJsonTesters
@WebMvcTest(SuperHeroController.class)
public class SuperHeroControllerMockMvcWithContextTest {
@Autowired
private MockMvc mvc;
@MockBean
private SuperHeroRepository superHeroRepository;
// This object will be initialized thanks to @AutoConfigureJsonTesters
@Autowired
private JacksonTester<SuperHero> jsonSuperHero;
@Test
public void canRetrieveByIdWhenExists() throws Exception {
// given
given(superHeroRepository.getSuperHero(2))
.willReturn(new SuperHero("Rob", "Mannon", "RobotMan"));
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes/2")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
);
}
@Test
public void canRetrieveByIdWhenDoesNotExist() throws Exception {
// given
given(superHeroRepository.getSuperHero(2))
.willThrow(new NonExistingHeroException());
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes/2")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
assertThat(response.getContentAsString()).isEmpty();
}
@Test
public void canRetrieveByNameWhenExists() throws Exception {
// given
given(superHeroRepository.getSuperHero("RobotMan"))
.willReturn(Optional.of(new SuperHero("Rob", "Mannon", "RobotMan")));
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes?name=RobotMan")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
);
}
@Test
public void canRetrieveByNameWhenDoesNotExist() throws Exception {
// given
given(superHeroRepository.getSuperHero("RobotMan"))
.willReturn(Optional.empty());
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes?name=RobotMan")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo("null");
}
@Test
public void canCreateANewSuperHero() throws Exception {
// when
MockHttpServletResponse response = mvc.perform(
post("/superheroes").contentType(MediaType.APPLICATION_JSON).content(
jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
)).andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
}
@Test
public void headerIsPresent() throws Exception {
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes/2")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getHeaders("X-SUPERHERO-APP")).containsOnly("super-header");
}
}
Cuando se compara con el modo Standalone, estas son las principales diferencias:
SpringExtension
Esta vez, necesitamos usar una extensión de JUnit 5 llamada SpringExtension, proporcionada por Spring. Esta extensión causa la inicialización de parte del contexto de Spring. Cuando ejecutas el test, puedes ver al principio del trace cómo el contexto y los beans inyectados comienzan a cargarse. Nota que no necesitamos incluir esta extensión explícitamente ya que ya está incluida en todas las anotaciones de acceso directo @...Test de Spring. Ve el código fuente de la clase WebMvcTest para un ejemplo de cómo esta anotación está incluida allí.
Autoconfiguración de MockMVC
Con la anotación @WebMVCTest, nuestra instancia de MockMVC se auto-configura y se hace disponible en el contexto (así que podemos auto-cablearla, como ves abajo en el código). También especificamos la clase del controller que queremos testear en la anotación. Gracias a esta anotación, Spring cargará solo un contexto parcial (el controller y su configuración circundante, como Filter y Advice).
Como Spring es lo suficientemente inteligente para saber que nuestro Filter y el Controller Advice también deberían ser inyectados, no hay necesidad de configuración explícita en el método setup().
Sobrescribir beans para testing usando MockBean
En este caso, inyectamos un repositorio falso en el contexto de Spring usando la anotación @MockBean. Este bean de repositorio falso reemplazará la implementación real del repositorio.
Tampoco necesitamos referenciar nuestra clase controller aparte de la que está en la anotación WebMVCTest, ya que el controller se inyectará automáticamente en el contexto.
Sin llamadas al servidor
Ten en cuenta que las respuestas que estamos verificando siguen siendo falsas. No hay un servidor web involucrado en este test tampoco. En cualquier caso, es un test perfectamente válido ya que estamos verificando nuestra lógica dentro de nuestra clase y, al mismo tiempo, alguna lógica relacionada dentro de SuperHeroExceptionHandler y SuperHeroFilter.
Usar MockMVC con un Web Application Context – Conclusiones
La diferencia más importante de este enfoque es que no necesitamos cargar explícitamente la lógica circundante ya que hay un contexto parcial de Spring en su lugar. Si creamos nuevos filtros, nuevos Advices de controller, o cualquier otra lógica participando en el proceso petición-respuesta, los inyectaremos automáticamente en nuestro test. Por lo tanto, no necesitamos manejar la configuración manual aquí. No hay control fino sobre qué usar en nuestro test, pero está más cerca de lo que sucede en realidad. Cuando ejecutamos nuestra aplicación, todo este material está allí por defecto.
Puedes ver una ligera transición al Integration Testing aquí. En este caso, estamos testeando el filtro y el controller advice listo para usar sin referenciarlos. Si incluimos cualquier otra clase interviniendo en el flujo petición-respuesta en el futuro, también participará en este test.
Como este test incluye el comportamiento de más de una clase, podrías clasificarlo como un Integration Test entre esas clases. La línea es difusa, sin embargo: podrías argumentar que solo un Controller está siendo testeado, pero necesitas la configuración extra para testearlo apropiadamente.
Tests Fuera del Servidor
Cuando realizas una petición HTTP a tu aplicación para testearla, estás ejecutando un test fuera del servidor. Sin embargo, incluso estando fuera, puedes inyectar mocks en estos tests, así que podrías obtener algo similar a un Unit Test en este caso. Por ejemplo, en una aplicación clásica de 3 capas, podrías mockear la capa de Servicio y testear solo tu Controller a través de un servidor web. En la práctica, este enfoque es mucho más pesado que un Unit Test puro. A menos que le digas a Spring que no lo haga excluyendo configuración o solo incluyendo lo que necesitas, estás cargando el contexto completo de la aplicación.
En Spring, puedes escribir tests fuera del servidor para controllers REST usando un RestTemplate estándar para realizar tus peticiones o el TestRestTemplate específico para tests, que incluye algunas características útiles para testing de integración (como la capacidad de incluir headers de autenticación).
En Spring Boot, usualmente usas la anotación @SpringBootTest cuando creas tests que usan el contexto. Luego, obtienes tus beans inyectados en el contexto, acceso a las propiedades de la aplicación, etc. Es una alternativa conveniente a la anotación clásica @ContextConfiguration en Spring que incluye todas las características de Spring Boot para tu test.
Dado el número de características y opciones disponibles, las estrategias de testing en Spring Boot pueden ser confusas. Examinémoslas de la misma manera que hicimos para MockMVC.
Estrategia 3: Ejemplo de SpringBootTest con un valor MOCK de WebEnvironment
Si usas @SpringBootTest sin parámetros o con webEnvironment = WebEnvironment.MOCK, no cargas un servidor HTTP real. ¿Suena familiar? Es un enfoque similar a la estrategia 2 (MockMVC con un contexto de aplicación). Cuando usamos esta configuración, aún estamos codificando un test dentro del servidor.
No podemos usar un RestTemplate estándar en esta configuración ya que no tenemos un servidor web. En su lugar, necesitamos seguir usando MockMVC, que ahora está siendo configurado gracias a la anotación extra @AutoconfigureMockMVC. Este enfoque es el más complicado de todos los disponibles, y desaconsejo usarlo. En su lugar, es mejor elegir la Estrategia 2 con MockMVC y el contexto cargado para un controller específico. Tendrás más control de lo que estás testeando.
Ve abajo los cambios en el código fuente cuando usamos esta estrategia.
@AutoConfigureJsonTesters
@SpringBootTest
@AutoConfigureMockMvc
public class SuperHeroControllerSpringBootMockTest {
@Autowired
private MockMvc mvc;
@MockBean
private SuperHeroRepository superHeroRepository;
// This object will be initialized thanks to @AutoConfigureJsonTesters
@Autowired
private JacksonTester<SuperHero> jsonSuperHero;
@Test
public void canRetrieveByIdWhenExists() throws Exception {
// given
given(superHeroRepository.getSuperHero(2))
.willReturn(new SuperHero("Rob", "Mannon", "RobotMan"));
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes/2")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
);
}
@Test
public void canRetrieveByIdWhenDoesNotExist() throws Exception {
// given
given(superHeroRepository.getSuperHero(2))
.willThrow(new NonExistingHeroException());
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes/2")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
assertThat(response.getContentAsString()).isEmpty();
}
@Test
public void canRetrieveByNameWhenExists() throws Exception {
// given
given(superHeroRepository.getSuperHero("RobotMan"))
.willReturn(Optional.of(new SuperHero("Rob", "Mannon", "RobotMan")));
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes?name=RobotMan")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
);
}
@Test
public void canRetrieveByNameWhenDoesNotExist() throws Exception {
// given
given(superHeroRepository.getSuperHero("RobotMan"))
.willReturn(Optional.empty());
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes?name=RobotMan")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo("null");
}
@Test
public void canCreateANewSuperHero() throws Exception {
// when
MockHttpServletResponse response = mvc.perform(
post("/superheroes/")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
}
@Test
public void headerIsPresent() throws Exception {
// when
MockHttpServletResponse response = mvc.perform(
get("/superheroes/2")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getHeader("X-SUPERHERO-APP")).isEqualTo("super-header");
}
}
Estrategia 4: Ejemplo de SpringBootTest con un Servidor Web Real
Cuando usas @SpringBootTest con WebEnvironment.RANDOM_PORT o WebEnvironment.DEFINED_PORT), estás testeando con un servidor HTTP real. En este caso, debes usar un RestTemplate o TestRestTemplate para realizar las peticiones.
La diferencia entre usar un puerto aleatorio o definido es que en el primer caso, el puerto por defecto 8080 (o el que sobrescribas con la propiedad server.port) no se usará sino que será reemplazado con un número de puerto asignado aleatoriamente. Esto es útil cuando ejecutas tests paralelos para evitar colisión de puertos. Veamos el código y luego describamos las características principales.
Ejemplo de Código Spring Boot Test
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SuperHeroControllerSpringBootTest {
@MockBean
private SuperHeroRepository superHeroRepository;
@Autowired
private TestRestTemplate restTemplate;
@Test
public void canRetrieveByIdWhenExists() {
// given
given(superHeroRepository.getSuperHero(2))
.willReturn(new SuperHero("Rob", "Mannon", "RobotMan"));
// when
ResponseEntity<SuperHero> superHeroResponse = restTemplate.getForEntity("/superheroes/2", SuperHero.class);
// then
assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(superHeroResponse.getBody().equals(new SuperHero("Rob", "Mannon", "RobotMan")));
}
@Test
public void canRetrieveByIdWhenDoesNotExist() {
// given
given(superHeroRepository.getSuperHero(2))
.willThrow(new NonExistingHeroException());
// when
ResponseEntity<SuperHero> superHeroResponse = restTemplate.getForEntity("/superheroes/2", SuperHero.class);
// then
assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(superHeroResponse.getBody()).isNull();
}
@Test
public void canRetrieveByNameWhenExists() {
// given
given(superHeroRepository.getSuperHero("RobotMan"))
.willReturn(Optional.of(new SuperHero("Rob", "Mannon", "RobotMan")));
// when
ResponseEntity<SuperHero> superHeroResponse = restTemplate
.getForEntity("/superheroes?name=RobotMan", SuperHero.class);
// then
assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(superHeroResponse.getBody()).isEqualTo(new SuperHero("Rob", "Mannon", "RobotMan"));
}
@Test
public void canRetrieveByNameWhenDoesNotExist() {
// given
given(superHeroRepository.getSuperHero("RobotMan"))
.willReturn(Optional.empty());
// when
ResponseEntity<SuperHero> superHeroResponse = restTemplate
.getForEntity("/superheroes?name=RobotMan", SuperHero.class);
// then
assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(superHeroResponse.getBody()).isNull();
}
@Test
public void canCreateANewSuperHero() {
// when
ResponseEntity<SuperHero> superHeroResponse = restTemplate.postForEntity("/superheroes",
new SuperHero("Rob", "Mannon", "RobotMan"), SuperHero.class);
// then
assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
@Test
public void headerIsPresent() throws Exception {
// when
ResponseEntity<SuperHero> superHeroResponse = restTemplate.getForEntity("/superheroes/2", SuperHero.class);
// then
assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(superHeroResponse.getHeaders().get("X-SUPERHERO-APP")).containsOnly("super-header");
}
}
Podemos enfocarnos ahora en las diferencias.
Testing de Servidor Web
Ahora, anotamos nuestro test con @SpringBootTest, y un modo de web-environment RANDOM_PORT. Al hacer eso, obtendremos un servidor web funcionando para nuestros tests.
En este caso, disparamos las peticiones usando un REST template (línea 18); más específicamente, su extensión TestRestTemplate, como si estuviéramos intentando alcanzar un servidor externo.
Las aserciones han cambiado un poco ya que la respuesta que queremos verificar es ahora un ResponseEntity en lugar de un MockHttpServletResponse. Sin embargo, son muy similares.
Mockeando capas
Nota que aún podemos mockear la capa de repositorio con la anotación @MockBean.
TestRestTemplate
Tenemos un bean TestRestTemplate en el contexto porque estamos usando la anotación @SpringBootTest. Esta versión de test se comporta precisamente igual que un RestTemplate estándar pero tiene algunas funcionalidades extra como se vio antes.
Enfoque SpringBootTest – Conclusiones
Aunque nuestro objetivo es el mismo – testear la capa Controller, este test aborda la solución desde un ángulo diferente que el de la Estrategia 1 (MockMVC en modo standalone). Antes, estábamos cargando nuestra clase y ni siquiera los actores circundantes (filter y controller advice). Aquí, estamos cargando el contexto completo de Spring Boot con el servidor web incluido. Este enfoque es el más pesado de todos y el más alejado del concepto de un Unit Test.
Esta estrategia está principalmente destinada para Integration Tests. Aún puedes mockear beans y reemplazarlos en el contexto. También puedes verificar interacciones entre diferentes clases en tu aplicación Spring Boot con un servidor web que participa en el proceso.
Yo evitaría usar esta estrategia para Unit Tests. Estás construyendo tests gordos y también puedes perder control de lo que estás testeando. Sin embargo, deberías favorecer este enfoque para Integration Tests. Estos tests son muy útiles para verificar cómo los diferentes componentes trabajan juntos en tu aplicación.
Rendimiento y Caché de Contexto
Puedes pensar que la Estrategia 1 es mucho más óptima en rendimiento que las otras. O que la Estrategia 4 puede tener un rendimiento terrible si necesitas cargar el Contexto completo de Spring Boot cada vez que ejecutas un test. Bueno, eso no es completamente correcto. Cuando usas Spring (incluyendo Boot) para testing, el contexto de aplicación se reutilizará por defecto durante la misma Suite de Tests.
En nuestro caso, las Estrategias 2, 3, y 4 reutilizan el contexto de Spring después de que se carga por primera vez. Sin embargo, recuerda que reutilizar el contexto puede causar efectos secundarios si tus tests modifican beans de Spring. Si ese es tu caso, necesitarás hacer algunos trucos con el @DirtiesContext, indicando que quieres recargar el contexto antes de cada test (ve la documentación si tienes curiosidad).
Conclusión
Como vimos, tenemos muchas alternativas para construir Unit Tests para nuestros Controllers en Spring Boot. Revisamos desde el más ligero al más pesado. Es hora de darte algunas recomendaciones sobre cuándo usar qué:
- Siempre escribe Unit Tests para tu lógica de Controller sin enfocarte en otros comportamientos. Elige la Estrategia 1: usa MockMVC en modo Standalone.
- Si necesitas testear algún comportamiento circundante relacionado con la Capa Web (como filtrado, interceptores, autenticación, etc.), elige la Estrategia 4: SpringBootTest con un servidor web en un puerto aleatorio. Sin embargo, trátalo como un Integration Test ya que estás verificando varias partes de tu aplicación. No omitas el Unit Test para la capa pura de Controller si es necesario. En otras palabras, trata de evitar mezclar las capas de test pero mantenlas separadas.
Espero que encuentres esta guía valiosa. ¡Por favor déjame saber si tienes algún feedback a través de comentarios! Si quieres aprender más sobre Spring Boot y cómo construir microservicios desde cero, revisa mi libro.
Get More Architecture Insights
Join hundreds of software architects and engineers getting practical insights delivered to their inbox.