Volver a Spring Boot Básico
Pruebas en Aplicaciones Spring Boot
Spring Boot proporciona soporte de primera clase para pruebas con spring-boot-starter-test, que incluye JUnit 5, Mockito y AssertJ.
Dependencia
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Pruebas Unitarias de Servicios
@ExtendWith(MockitoExtension.class)
class ProductoServicioTest {
@Mock
private ProductoRepositorio productoRepositorio;
@InjectMocks
private ProductoServicio productoServicio;
@Test
void buscarPorId_CuandoExiste_DevuelveProducto() {
// Preparar
Producto producto = new Producto(1L, "Laptop", new BigDecimal("999.99"));
when(productoRepositorio.findById(1L)).thenReturn(Optional.of(producto));
// Actuar
Producto resultado = productoServicio.buscarPorId(1L);
// Verificar
assertThat(resultado.getNombre()).isEqualTo("Laptop");
assertThat(resultado.getPrecio()).isEqualByComparingTo("999.99");
verify(productoRepositorio, times(1)).findById(1L);
}
@Test
void buscarPorId_CuandoNoExiste_LanzaExcepcion() {
when(productoRepositorio.findById(99L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> productoServicio.buscarPorId(99L))
.isInstanceOf(RecursoNoEncontradoException.class)
.hasMessageContaining("99");
}
@Test
void guardar_ProductoValido_DevuelveProductoGuardado() {
Producto producto = new Producto(null, "Teléfono", new BigDecimal("699.99"));
Producto guardado = new Producto(1L, "Teléfono", new BigDecimal("699.99"));
when(productoRepositorio.save(producto)).thenReturn(guardado);
Producto resultado = productoServicio.guardar(producto);
assertThat(resultado.getId()).isEqualTo(1L);
verify(productoRepositorio).save(producto);
}
}
Pruebas de Repositorio con @DataJpaTest
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
class ProductoRepositorioTest {
@Autowired
private ProductoRepositorio productoRepositorio;
@Autowired
private TestEntityManager entityManager;
@Test
void buscarPorCategoria_DevuelveProductosCorrespondientes() {
entityManager.persistAndFlush(new Producto(null, "Laptop", Categoria.ELECTRONICA));
entityManager.persistAndFlush(new Producto(null, "Teléfono", Categoria.ELECTRONICA));
entityManager.persistAndFlush(new Producto(null, "Camisa", Categoria.ROPA));
List<Producto> electronica = productoRepositorio.findByCategoria(Categoria.ELECTRONICA);
assertThat(electronica).hasSize(2);
assertThat(electronica).extracting(Producto::getNombre)
.containsExactlyInAnyOrder("Laptop", "Teléfono");
}
@Test
void buscarEnRangoPrecio_DevuelveEntreMinimoYMaximo() {
entityManager.persistAndFlush(new Producto(null, "Económico", new BigDecimal("100")));
entityManager.persistAndFlush(new Producto(null, "Medio", new BigDecimal("500")));
entityManager.persistAndFlush(new Producto(null, "Alto", new BigDecimal("2000")));
List<Producto> resultado = productoRepositorio.buscarEnRangoPrecio(
new BigDecimal("200"), new BigDecimal("1000"));
assertThat(resultado).hasSize(1);
assertThat(resultado.get(0).getNombre()).isEqualTo("Medio");
}
}
Pruebas de Controlador con @WebMvcTest
@WebMvcTest(ProductoControlador.class)
@Import(ConfiguracionSeguridad.class)
class ProductoControladorTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductoServicio productoServicio;
@Autowired
private ObjectMapper objectMapper;
@Test
void obtenerPorId_ProductoExistente_Retorna200() throws Exception {
Producto producto = new Producto(1L, "Laptop", new BigDecimal("999.99"));
when(productoServicio.buscarPorId(1L)).thenReturn(producto);
mockMvc.perform(get("/api/productos/1")
.header("Authorization", "Bearer " + tokenValido()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.nombre").value("Laptop"))
.andExpect(jsonPath("$.precio").value(999.99));
}
@Test
void crear_CuerpoInvalido_Retorna400() throws Exception {
String cuerpo = """
{
"nombre": "",
"precio": -10
}
""";
mockMvc.perform(post("/api/productos")
.contentType(MediaType.APPLICATION_JSON)
.content(cuerpo)
.header("Authorization", "Bearer " + tokenValido()))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.codigo").value("ERROR_VALIDACION"));
}
}
Pruebas de Integración con @SpringBootTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class ProductoIntegracionTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ProductoRepositorio productoRepositorio;
@BeforeEach
void setUp() {
productoRepositorio.deleteAll();
}
@Test
void flujoCompletoCRUD() {
// Crear
CrearProductoRequest request = new CrearProductoRequest("Laptop", new BigDecimal("999"));
ResponseEntity<Producto> creado = restTemplate
.withBasicAuth("[email protected]", "contrasena")
.postForEntity("/api/productos", request, Producto.class);
assertThat(creado.getStatusCode()).isEqualTo(HttpStatus.CREATED);
Long productoId = creado.getBody().getId();
// Leer
ResponseEntity<Producto> obtenido = restTemplate
.getForEntity("/api/productos/" + productoId, Producto.class);
assertThat(obtenido.getBody().getNombre()).isEqualTo("Laptop");
// Eliminar
restTemplate.delete("/api/productos/" + productoId);
assertThat(productoRepositorio.existsById(productoId)).isFalse();
}
}
Resumen de Slices de Prueba
| Anotación | Qué carga |
|---|---|
@SpringBootTest | Contexto completo de la aplicación |
@WebMvcTest | Solo capa de controlador (sin BD) |
@DataJpaTest | Repositorios JPA + BD en memoria |
@JsonTest | Solo serialización JSON |
@MockBean | Agrega un mock Mockito al contexto Spring |
Configuración de Pruebas
# src/test/resources/application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop
logging.level.org.springframework.security=OFF