Skip to content

13. Testing en Spring

TipoDescripciónVelocidadCobertura
UnitariasPrueban una clase aisladaMuy rápidasBaja
IntegraciónPrueban múltiples componentesLentasAlta
E2EPrueban todo el sistemaMuy lentasCompleta
Dependencia de testing
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Incluye: JUnit 5, Mockito, AssertJ, Hamcrest, JSONPath -->
Pirámide de testing
/\
/ \
/ E2E \ ← Pocas pruebas
/________\
/ \
/ Integración\ ← Algunas pruebas
/______________\
/ \
/ Unitarias \ ← Muchas pruebas
/____________________\

Test unitario básico
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.*;
class CalculadoraTest {
private Calculadora calculadora;
@BeforeEach
void setUp() {
calculadora = new Calculadora();
}
@Test
@DisplayName("Suma de dos números positivos")
void testSumar() {
int resultado = calculadora.sumar(2, 3);
assertEquals(5, resultado);
}
@Test
@DisplayName("División por cero lanza excepción")
void testDivisionPorCero() {
assertThrows(ArithmeticException.class, () -> {
calculadora.dividir(10, 0);
});
}
@Test
void testMultiplesAserciones() {
assertAll(
() -> assertEquals(4, calculadora.sumar(2, 2)),
() -> assertEquals(0, calculadora.sumar(-2, 2)),
() -> assertEquals(-4, calculadora.sumar(-2, -2))
);
}
}
AssertJ
import static org.assertj.core.api.Assertions.*;
class UsuarioServiceTest {
@Test
void testCrearUsuario() {
Usuario usuario = service.crear("Juan", "juan@email.com");
assertThat(usuario)
.isNotNull()
.extracting(Usuario::getNombre, Usuario::getEmail)
.containsExactly("Juan", "juan@email.com");
}
@Test
void testListarUsuarios() {
List<Usuario> usuarios = service.listar();
assertThat(usuarios)
.hasSize(3)
.extracting(Usuario::getNombre)
.contains("Juan", "María", "Pedro");
}
@Test
void testExcepcion() {
assertThatThrownBy(() -> service.buscar(999L))
.isInstanceOf(UsuarioNotFoundException.class)
.hasMessageContaining("no encontrado");
}
}

Mockito básico
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.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UsuarioServiceTest {
@Mock
private UsuarioRepository repository;
@Mock
private EmailService emailService;
@InjectMocks
private UsuarioService service;
@Test
void testCrearUsuario() {
// Arrange
UsuarioDTO dto = new UsuarioDTO("Juan", "juan@email.com");
Usuario usuarioGuardado = new Usuario(1L, "Juan", "juan@email.com");
when(repository.save(any(Usuario.class))).thenReturn(usuarioGuardado);
// Act
Usuario resultado = service.crear(dto);
// Assert
assertThat(resultado.getId()).isEqualTo(1L);
verify(repository).save(any(Usuario.class));
verify(emailService).enviarBienvenida("juan@email.com");
}
}
Stubbing avanzado
@Test
void testBuscarUsuario() {
// Retornar valor específico
when(repository.findById(1L)).thenReturn(Optional.of(usuario));
when(repository.findById(999L)).thenReturn(Optional.empty());
// Retornar basado en argumento
when(repository.findByEmail(anyString()))
.thenAnswer(invocation -> {
String email = invocation.getArgument(0);
return Optional.of(new Usuario(email));
});
// Lanzar excepción
when(repository.findById(-1L))
.thenThrow(new IllegalArgumentException("ID inválido"));
// Múltiples llamadas
when(repository.count())
.thenReturn(10L)
.thenReturn(11L)
.thenReturn(12L);
}
Verificaciones
@Test
void testVerificaciones() {
service.procesarUsuarios();
// Verificar que se llamó
verify(repository).findAll();
// Verificar número de llamadas
verify(emailService, times(3)).enviar(anyString());
verify(emailService, atLeast(1)).enviar(anyString());
verify(emailService, atMost(5)).enviar(anyString());
verify(emailService, never()).enviarUrgente(anyString());
// Verificar orden
InOrder inOrder = inOrder(repository, emailService);
inOrder.verify(repository).findAll();
inOrder.verify(emailService).enviar(anyString());
// Capturar argumentos
ArgumentCaptor<Usuario> captor = ArgumentCaptor.forClass(Usuario.class);
verify(repository).save(captor.capture());
Usuario usuarioCapturado = captor.getValue();
assertThat(usuarioCapturado.getNombre()).isEqualTo("Juan");
}

@SpringBootTest
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest // Carga todo el contexto de Spring
@ActiveProfiles("test")
class UsuarioServiceIntegrationTest {
@Autowired
private UsuarioService service;
@Autowired
private UsuarioRepository repository;
@BeforeEach
void setUp() {
repository.deleteAll();
}
@Test
void testCrearYBuscarUsuario() {
// Crear
Usuario creado = service.crear(new UsuarioDTO("Juan", "juan@email.com"));
// Buscar
Usuario encontrado = service.buscarPorId(creado.getId());
assertThat(encontrado.getNombre()).isEqualTo("Juan");
}
}
Configuración de test
# src/test/resources/application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

@WebMvcTest
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UsuarioController.class) // Solo carga el controller
class UsuarioControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // Mock del servicio
private UsuarioService service;
@Test
void testListarUsuarios() throws Exception {
List<Usuario> usuarios = List.of(
new Usuario(1L, "Juan"),
new Usuario(2L, "María")
);
when(service.listar()).thenReturn(usuarios);
mockMvc.perform(get("/api/usuarios"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$.length()").value(2))
.andExpect(jsonPath("$[0].nombre").value("Juan"));
}
@Test
void testCrearUsuario() throws Exception {
Usuario usuario = new Usuario(1L, "Juan", "juan@email.com");
when(service.crear(any())).thenReturn(usuario);
mockMvc.perform(post("/api/usuarios")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"nombre": "Juan",
"email": "juan@email.com"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.nombre").value("Juan"));
}
@Test
void testUsuarioNoEncontrado() throws Exception {
when(service.buscarPorId(999L))
.thenThrow(new UsuarioNotFoundException(999L));
mockMvc.perform(get("/api/usuarios/999"))
.andExpect(status().isNotFound());
}
}

@DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
@DataJpaTest // Solo carga JPA, usa H2 por defecto
class UsuarioRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UsuarioRepository repository;
@Test
void testFindByEmail() {
// Arrange
Usuario usuario = new Usuario("Juan", "juan@email.com");
entityManager.persistAndFlush(usuario);
// Act
Optional<Usuario> encontrado = repository.findByEmail("juan@email.com");
// Assert
assertThat(encontrado).isPresent();
assertThat(encontrado.get().getNombre()).isEqualTo("Juan");
}
@Test
void testFindByRol() {
entityManager.persist(new Usuario("Admin", "admin@email.com", Rol.ADMIN));
entityManager.persist(new Usuario("User1", "user1@email.com", Rol.USER));
entityManager.persist(new Usuario("User2", "user2@email.com", Rol.USER));
entityManager.flush();
List<Usuario> admins = repository.findByRol(Rol.ADMIN);
List<Usuario> users = repository.findByRol(Rol.USER);
assertThat(admins).hasSize(1);
assertThat(users).hasSize(2);
}
@Test
void testQueryPersonalizada() {
entityManager.persist(new Usuario("Juan", "juan@email.com", true));
entityManager.persist(new Usuario("María", "maria@email.com", false));
entityManager.flush();
List<Usuario> activos = repository.findUsuariosActivos();
assertThat(activos)
.hasSize(1)
.extracting(Usuario::getNombre)
.containsExactly("Juan");
}
}

Test de seguridad
import org.springframework.security.test.context.support.WithMockUser;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
@WebMvcTest(AdminController.class)
class AdminControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
void testAccesoSinAutenticacion() throws Exception {
mockMvc.perform(get("/api/admin/usuarios"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "USER")
void testAccesoConRolIncorrecto() throws Exception {
mockMvc.perform(get("/api/admin/usuarios"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
void testAccesoConRolAdmin() throws Exception {
mockMvc.perform(get("/api/admin/usuarios"))
.andExpect(status().isOk());
}
@Test
void testLoginConCredenciales() throws Exception {
mockMvc.perform(post("/api/auth/login")
.with(httpBasic("admin", "password")))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "juan@email.com", authorities = {"CREAR_USUARIOS"})
void testConAutoridadEspecifica() throws Exception {
mockMvc.perform(post("/api/usuarios")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isCreated());
}
}

Estructura de tests
// Patrón AAA (Arrange-Act-Assert)
@Test
void testCrearPedido() {
// Arrange (preparar)
Usuario usuario = new Usuario(1L, "Juan");
Producto producto = new Producto(1L, "Laptop", 999.99);
when(usuarioRepository.findById(1L)).thenReturn(Optional.of(usuario));
when(productoRepository.findById(1L)).thenReturn(Optional.of(producto));
// Act (ejecutar)
Pedido pedido = pedidoService.crear(1L, List.of(1L));
// Assert (verificar)
assertThat(pedido.getUsuario()).isEqualTo(usuario);
assertThat(pedido.getProductos()).hasSize(1);
assertThat(pedido.getTotal()).isEqualTo(999.99);
}
// Nombres descriptivos
@Test
@DisplayName("Crear pedido con productos agotados lanza StockException")
void crearPedido_conProductosAgotados_lanzaStockException() {
// ...
}
Testcontainers
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class UsuarioRepositoryRealDbTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void testConPostgresReal() {
// Test con PostgreSQL real en Docker
}
}
🐝