GUÍA COMPLETA PARA ESCRIBIR PRUEBAS UNITARIAS EN SPRING BOOT
Introducción a las pruebas unitarias en Spring Boot
Las pruebas unitarias son un pilar fundamental en el desarrollo de aplicaciones modernas, especialmente en frameworks robustos como Spring Boot. Garantizan que cada componente de la aplicación funcione correctamente de manera aislada, lo que reduce errores y facilita la evolución del código. En este tutorial, exploraremos cómo escribir pruebas unitarias efectivas para controladores, servicios y repositorios en una aplicación Spring Boot. A través de ejemplos prácticos, aprenderás a utilizar herramientas como MockMvc, Mockito y H2 para asegurar la calidad de tu código. Este artículo está diseñado para desarrolladores que buscan mejorar la fiabilidad de sus aplicaciones Spring Boot mediante pruebas automatizadas.
¿Por qué escribir pruebas unitarias?
Las pruebas unitarias son esenciales para proteger el código contra cambios que puedan romper la lógica existente. Cuando un desarrollador agrega una nueva funcionalidad sin pruebas, existe el riesgo de alterar reglas de negocio previas, lo que puede generar errores impredecibles. Las pruebas unitarias permiten validar que cada unidad de código, como un controlador, servicio o repositorio, cumpla con su propósito sin depender de otros componentes. Esto es especialmente crítico en proyectos complejos donde múltiples desarrolladores colaboran. Además, las pruebas unitarias automatizan la verificación, ahorrando tiempo frente a pruebas manuales y proporcionando confianza en el despliegue de nuevas versiones.
Pruebas unitarias para controladores
Los controladores en Spring Boot gestionan las solicitudes HTTP y devuelven respuestas al cliente. Probarlos asegura que las rutas, los formatos de respuesta y la lógica de manejo de solicitudes funcionen correctamente. Para probar controladores, usamos MockMvc, una herramienta que simula solicitudes HTTP sin necesidad de iniciar un servidor real. También empleamos Mockito para simular dependencias como servicios, lo que nos permite enfocarnos únicamente en la lógica del controlador.
Supongamos que tenemos una aplicación para gestionar usuarios, con una entidad User, un DTO CreateUserRequest y un controlador UserController. A continuación, se muestra un ejemplo de cómo estructurar estos componentes:
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.Column;
import org.hibernate.annotations.GenericGenerator;
import java.util.UUID;
@Data
@Entity
public class User {
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator")
@Column(name = "id", columnDefinition = "BINARY(16)")
private UUID id;
private String name;
private String email;
private int age;
}
import lombok.Data;
@Data
public class CreateUserRequest {
private String name;
private String email;
private int age;
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
User created = userService.save(request);
return ResponseEntity.ok(created);
}
}
Para probar este controlador, creamos una clase de prueba que utiliza MockMvc y Mockito. El siguiente ejemplo muestra cómo configurar una prueba unitaria para el endpoint POST /users:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
public void shouldCreateUserSuccessfully() throws Exception {
CreateUserRequest request = new CreateUserRequest();
request.setName("John Doe");
request.setEmail("[email protected]");
request.setAge(30);
User user = new User();
user.setId(UUID.randomUUID());
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setAge(request.getAge());
when(userService.save(any(CreateUserRequest.class))).thenReturn(user);
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value(request.getName()))
.andExpect(jsonPath("$.email").value(request.getEmail()))
.andExpect(jsonPath("$.age").value(request.getAge()));
}
}
En este ejemplo, simulamos el método save del servicio para devolver un objeto User. Usamos MockMvc para enviar una solicitud POST y verificamos que la respuesta tenga el código de estado 200 y los datos esperados en formato JSON. Herramientas como jsonPath permiten validar la estructura de la respuesta, asegurando que el controlador maneje correctamente la solicitud.
Pruebas unitarias para servicios
Los servicios contienen la lógica de negocio de la aplicación, por lo que es crucial probar que funcionen según lo esperado. Para probar servicios, usamos Mockito para simular dependencias como repositorios, permitiendo enfocarnos en la lógica del servicio sin interactuar con la base de datos.
Supongamos que tenemos un servicio UserService que guarda usuarios y valida que el correo electrónico no sea nulo:
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User save(CreateUserRequest request) {
Objects.requireNonNull(request.getEmail(), "Email cannot be null");
User user = new User();
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setAge(request.getAge());
return userRepository.save(user);
}
}
La prueba unitaria para este servicio podría verse así:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
public void shouldSaveUserSuccessfully() {
CreateUserRequest request = new CreateUserRequest();
request.setName("John Doe");
request.setEmail("[email protected]");
request.setAge(30);
User user = new User();
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setAge(request.getAge());
when(userRepository.save(any(User.class))).thenReturn(user);
User result = userService.save(request);
assertEquals(request.getName(), result.getName());
assertEquals(request.getEmail(), result.getEmail());
assertEquals(request.getAge(), result.getAge());
}
@Test
public void shouldThrowExceptionForNullEmail() {
CreateUserRequest request = new CreateUserRequest();
request.setName("John Doe");
request.setAge(30);
assertThrows(NullPointerException.class, () -> userService.save(request));
}
}
En esta prueba, simulamos el repositorio y verificamos dos casos: uno donde el guardado es exitoso y otro donde se lanza una excepción por un correo nulo. Esto asegura que la regla de negocio se cumpla y que el servicio interactúe correctamente con el repositorio.
Pruebas unitarias para repositorios
Los repositorios en Spring Boot gestionan la interacción con la base de datos. Aunque no probamos el framework subyacente (como Spring Data JPA), sí verificamos que nuestras consultas personalizadas y relaciones estén implementadas correctamente. Para pruebas de repositorios, usamos una base de datos en memoria como H2 junto con TestEntityManager para configurar datos de prueba.
Definamos un repositorio UserRepository:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import java.util.Optional;
import java.util.UUID;
public interface UserRepository extends JpaRepository<User, UUID>, JpaSpecificationExecutor<User> {
Optional<User> findByEmail(String email);
}
Para configurar H2, creamos un archivo application.yml en la carpeta src/test/resources:
spring:
application:
name: Spring Boot Rest API
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: "jdbc:h2:mem:test-api;INIT=CREATE SCHEMA IF NOT EXISTS dbo\\;CREATE SCHEMA IF NOT EXISTS definitions;DATABASE_TO_UPPER=false;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;MODE=MSSQLServer"
name:
password:
username:
initialization-mode: never
hikari:
schema: dbo
jpa:
database: H2
database-platform: org.hibernate.dialect.H2Dialect
show-sql: true
hibernate:
ddl-auto: create-drop
test:
database:
replace: none
Ahora, escribimos pruebas para el repositorio:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
public class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
public void shouldSaveAndRetrieveUser() {
User user = new User();
user.setName("Test User");
user.setEmail("[email protected]");
user.setAge(25);
user = entityManager.persistAndFlush(user);
assertThat(userRepository.findById(user.getId())).isPresent().hasValue(user);
}
@Test
public void shouldFindUserByEmail() {
User user = new User();
user.setName("Test User");
user.setEmail("[email protected]");
user.setAge(25);
user = entityManager.persistAndFlush(user);
assertThat(userRepository.findByEmail("[email protected]")).isPresent().hasValue(user);
}
}
Estas pruebas verifican que el repositorio puede guardar un usuario y recuperarlo por su ID o correo electrónico. La base de datos H2 se inicializa y destruye automáticamente para cada prueba, asegurando un entorno limpio. Al ejecutar las pruebas, Hibernate genera consultas SQL como la siguiente:
SELECT user0_.id AS id1_1_, user0_.age AS age2_1_, user0_.email AS email3_1_, user0_.name AS name4_1_ FROM user user0_ WHERE user0_.email=?
Mejores prácticas para pruebas unitarias
Para maximizar la efectividad de las pruebas unitarias en Spring Boot, considera las siguientes recomendaciones:
- Aislar componentes: Usa herramientas como Mockito para simular dependencias y probar cada componente de forma aislada.
- Cubrir casos extremos: Escribe pruebas para escenarios de error, como entradas nulas o inválidas, para garantizar que el código sea robusto.
- Usar bases de datos en memoria: H2 es ideal para pruebas de repositorios, ya que es rápida y no requiere configuración externa.
- Mantener pruebas legibles: Usa nombres descriptivos para los métodos de prueba, como
shouldSaveUserSuccessfully, para facilitar la comprensión. - Ejecutar pruebas frecuentemente: Integra las pruebas en un sistema de integración continua para detectar errores rápidamente.
Al seguir estas prácticas, puedes construir un conjunto de pruebas robusto que proteja tu aplicación contra errores y facilite su mantenimiento.
Conclusiones
Las pruebas unitarias son una herramienta indispensable para desarrollar aplicaciones Spring Boot confiables y mantenibles. Al probar controladores, servicios y repositorios, garantizas que cada componente funcione correctamente y que las reglas de negocio se mantengan intactas. Herramientas como MockMvc, Mockito y H2 simplifican la creación de pruebas efectivas, mientras que las mejores prácticas aseguran que tus pruebas sean claras y eficientes. Implementar un enfoque riguroso de pruebas unitarias no solo mejora la calidad del código, sino que también ahorra tiempo al reducir la necesidad de depuración manual. Con los ejemplos proporcionados, puedes comenzar a escribir pruebas unitarias robustas y proteger tus aplicaciones Spring Boot contra errores futuros.