Vamos a escribir dos tipos de tests:
- Unit-tests: verifican métodos y componentes aislados. Nada de trabajar con base de datos o servicios externos. Usamos mocks (Mockito) para simular dependencias.
- Pruebas de integración: verifican el trabajo de varios componentes juntos. Por ejemplo, la interacción de controllers con services y la base de datos.
Preparación para las pruebas
Dependencias para testing
Spring Boot ya incluye todo lo necesario para testing básico. Sin embargo, conviene asegurarse de que en tu pom.xml estén añadidas estas dependencias:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
Si usas Gradle, las líneas equivalentes se verían así:
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.testcontainers:testcontainers'
Después de añadir las dependencias, asegúrate de que el proyecto se recarga correctamente.
Unit-tests
Empezamos escribiendo Unit-tests para la capa de servicios.
Ejemplo de código: test del servicio
Supongamos que tenemos la siguiente lógica de negocio en nuestro servicio:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
}
Testear usando Mockito
Para probar esta clase necesitamos aislarla de la base de datos. Esto se consigue con mocks:
@ExtendWith(MockitoExtension.class) // Activamos la integración con Mockito
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldReturnUserWhenFound() {
// Arrange
Long userId = 1L;
User mockUser = new User(userId, "John Doe", "john.doe@example.com");
when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); // Mockeamos el comportamiento del repositorio
// Act
User result = userService.findUserById(userId);
// Assert
assertNotNull(result);
assertEquals("John Doe", result.getName());
verify(userRepository, times(1)).findById(userId); // Verificamos que el método se llamó exactamente una vez
}
@Test
void shouldThrowExceptionWhenUserNotFound() {
// Arrange
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// Act & Assert
assertThrows(UserNotFoundException.class, () -> userService.findUserById(userId));
}
}
¿Qué ocurre aquí?
- Usamos la anotación
@Mockpara crear un UserRepository "falso". - Con
@InjectMocksinyectamos ese mock en UserService. - En los tests configuramos el comportamiento del repositorio (
when(...).thenReturn(...)) para crear un entorno controlado. - Nos aseguramos de que el método funciona correctamente y lanza la excepción si el usuario no se encuentra.
Pruebas de integración
Ahora vamos a casos más complejos, cuando necesitamos comprobar la interacción de componentes. Usaremos una base de datos real (por ejemplo, H2) y probaremos el funcionamiento de controllers y services.
Configuración de pruebas de integración
Para las pruebas de integración de controllers, Spring Boot ofrece la anotación @SpringBootTest. Levanta el contexto de la aplicación y permite probar la interacción entre capas.
Ejemplo de REST controller a probar:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.findUserById(id);
return ResponseEntity.ok(user);
}
}
Test de integración del controller
Escribimos un test que hace llamadas directas a nuestra API:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // Ejecutamos con servidor web
@AutoConfigureMockMvc // Configuramos MockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldReturnUserWhenExists() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
@Test
void shouldReturnNotFoundWhenUserDoesNotExist() throws Exception {
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
}
Depuración de tests y errores típicos
- Error de configuración del contexto: asegúrate de que tu configuración de
@SpringBootTesty los mocks estén correctamente configurados. - Carga perezosa de datos: a veces las pruebas de integración pueden fallar porque las relaciones entre entidades se cargan de forma lazy. Esto se soluciona configurando carga explícita (FetchType.EAGER) o usando DTOs.
- Tests lentos: si tus tests son lentos, considera usar Testcontainers para aislar los tests localmente en contenedores.
Automatización de las pruebas
Integra los tests en el pipeline CI/CD. En cada nuevo commit tus Unit tests y pruebas de integración deben ejecutarse para evitar que bugs lleguen a la rama principal. La mayoría de herramientas (Jenkins, GitLab CI, GitHub Actions) se integran fácilmente con Maven o Gradle.
# Ejemplo de pipeline de GitLab CI
stages:
- test
test:
script:
- ./mvnw test
Así puedes tener confianza en tu aplicación antes de desplegarla. Ahora sabes cómo escribir tanto Unit tests como pruebas de integración. ¿Listo para el siguiente paso? ¡Es hora de meterse en containerización y automatizar el build! 🚀
GO TO FULL VERSION