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ónQué carga
@SpringBootTestContexto completo de la aplicación
@WebMvcTestSolo capa de controlador (sin BD)
@DataJpaTestRepositorios JPA + BD en memoria
@JsonTestSolo serialización JSON
@MockBeanAgrega 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