Pruebas automatizadas para microservicios con Spring Boot

Capítulo 9

Tiempo estimado de lectura: 9 minutos

+ Ejercicio

Pirámide de pruebas aplicada a microservicios

En microservicios, el objetivo de las pruebas automatizadas no es “probarlo todo en extremo a extremo” (costoso y frágil), sino construir una pirámide: muchas pruebas rápidas y deterministas en la base, menos pruebas de integración en el medio, y un conjunto pequeño pero crítico de pruebas de contrato para asegurar compatibilidad entre servicios. Una pirámide típica para Spring Boot puede quedar así:

  • Unitarias (dominio y lógica de aplicación): sin Spring, sin red, sin base de datos. Ejecutan en milisegundos.
  • Integración (persistencia y web): con Spring Test, base de datos efímera o contenedorizada, y capa web con MockMvc/WebTestClient.
  • Contrato (APIs): verifican que productor y consumidor comparten expectativas (request/response, headers, códigos, errores).

La regla práctica: si una prueba falla, debe decirte qué se rompió y dónde, sin depender del orden de ejecución ni de servicios externos inestables.

Dependencias y herramientas recomendadas

Dependencias típicas (Gradle) para JUnit 5, Spring Test, Mockito y Testcontainers:

dependencies {  testImplementation 'org.springframework.boot:spring-boot-starter-test'  testImplementation 'org.testcontainers:junit-jupiter:1.19.7'  testImplementation 'org.testcontainers:postgresql:1.19.7'  // Contratos (ejemplo con Spring Cloud Contract)  testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier'}

En Maven, las coordenadas son equivalentes. Asegúrate de usar el BOM de Spring Cloud si incorporas Spring Cloud Contract.

1) Pruebas unitarias: dominio y casos de uso (sin Spring)

Qué probar aquí

  • Reglas de negocio puras (cálculos, invariantes, estados).
  • Casos de uso/servicios de aplicación con dependencias mockeadas (repositorios, clientes HTTP, colas).
  • Mapeos y validaciones internas que no dependan de infraestructura.

Ejemplo: caso de uso con dependencias simuladas

Supón un servicio de aplicación que crea un pedido y consulta un servicio externo de inventario. La prueba debe ser determinista: inventario simulado, reloj controlado, IDs predecibles.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

class OrderServiceTest {  private final InventoryClient inventoryClient = Mockito.mock(InventoryClient.class);  private final OrderRepository orderRepository = Mockito.mock(OrderRepository.class);  private final Clock fixedClock = Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);  private final OrderService service = new OrderService(inventoryClient, orderRepository, fixedClock);  @Test  void createsOrder_whenStockIsAvailable() {    Mockito.when(inventoryClient.hasStock("SKU-1", 2)).thenReturn(true);    Mockito.when(orderRepository.save(Mockito.any())).thenAnswer(inv -> inv.getArgument(0));    Order order = service.create("customer-1", "SKU-1", 2);    assertEquals("customer-1", order.customerId());    assertEquals("SKU-1", order.sku());    assertEquals(2, order.quantity());    assertEquals(Instant.parse("2026-01-01T00:00:00Z"), order.createdAt());    Mockito.verify(inventoryClient).hasStock("SKU-1", 2);    Mockito.verify(orderRepository).save(Mockito.any(Order.class));  }  @Test  void rejectsOrder_whenNoStock() {    Mockito.when(inventoryClient.hasStock("SKU-1", 2)).thenReturn(false);    assertThrows(OutOfStockException.class, () -> service.create("customer-1", "SKU-1", 2));    Mockito.verify(orderRepository, Mockito.never()).save(Mockito.any());  }}

Claves anti-flakiness: usa Clock.fixed para fechas, evita aleatoriedad, y no dependas del orden de tests. Si necesitas IDs, inyecta un generador (por ejemplo, Supplier<UUID>) y fíltralo en pruebas.

2) Pruebas de integración: persistencia (repositorios) con DB efímera o contenedorizada

Opciones para la base de datos

  • Efímera en memoria (p. ej. H2): rápida, pero puede diferir de tu motor real (tipos, SQL, constraints).
  • Contenedorizada (Testcontainers con PostgreSQL/MySQL): más fiel al entorno real, ideal para evitar sorpresas.

Para microservicios, suele compensar usar Testcontainers en integración de persistencia.

Ejemplo con Testcontainers + Spring Boot

Prueba un repositorio JPA verificando constraints y consultas. Usa @DataJpaTest para levantar solo la capa de persistencia.

@Testcontainers @DataJpaTest class OrderRepositoryIT {  @Container  static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")      .withDatabaseName("testdb")      .withUsername("test")      .withPassword("test");  @DynamicPropertySource  static void props(DynamicPropertyRegistry registry) {    registry.add("spring.datasource.url", postgres::getJdbcUrl);    registry.add("spring.datasource.username", postgres::getUsername);    registry.add("spring.datasource.password", postgres::getPassword);    registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");  }  @Autowired OrderRepository repository;  @Test  void savesAndFindsByCustomerId() {    OrderEntity e = new OrderEntity();    e.setId(UUID.fromString("00000000-0000-0000-0000-000000000001"));    e.setCustomerId("customer-1");    e.setSku("SKU-1");    e.setQuantity(2);    repository.saveAndFlush(e);    List<OrderEntity> found = repository.findByCustomerId("customer-1");    assertEquals(1, found.size());    assertEquals("SKU-1", found.get(0).getSku());  }}

Datos deterministas: fija UUIDs, usa valores constantes, y evita depender de autoincrementos si luego asertes exactos. Si necesitas autoincrementos, aserta propiedades funcionales (p. ej. “no es null”) en lugar de valores exactos.

Validación de errores de persistencia

También es útil probar que constraints se aplican (por ejemplo, NOT NULL o unicidad). En JPA, muchas violaciones aparecen al hacer flush:

@Test void failsOnNullSku() {  OrderEntity e = new OrderEntity();  e.setId(UUID.fromString("00000000-0000-0000-0000-000000000002"));  e.setCustomerId("customer-1");  e.setSku(null);  e.setQuantity(1);  repository.save(e);  assertThrows(DataIntegrityViolationException.class, () -> repository.flush());}

3) Pruebas de integración: capa web (controladores) con dependencias mockeadas

Cuándo usar @WebMvcTest

@WebMvcTest carga el contexto web (controladores, Jackson, validación, filtros MVC) sin levantar toda la aplicación. Es ideal para probar:

  • Rutas, parámetros, headers, códigos HTTP.
  • Serialización/deserialización JSON.
  • Validaciones (@Valid) y manejo de errores.
  • Interacción del controlador con su servicio (mock).

Ejemplo: prueba de controlador con MockMvc

Controlador que delega en un servicio. Se mockea el servicio con @MockBean.

@WebMvcTest(OrderController.class) class OrderControllerTest {  @Autowired MockMvc mvc;  @MockBean OrderService orderService;  @Test  void createsOrder_returns201AndBody() throws Exception {    Order created = new Order("order-1", "customer-1", "SKU-1", 2);    Mockito.when(orderService.create(Mockito.any())).thenReturn(created);    mvc.perform(post("/orders")        .contentType("application/json")        .content("{\"customerId\":\"customer-1\",\"sku\":\"SKU-1\",\"quantity\":2}"))      .andExpect(status().isCreated())      .andExpect(header().string("Location", "/orders/order-1"))      .andExpect(jsonPath("$.id").value("order-1"))      .andExpect(jsonPath("$.sku").value("SKU-1"));  }}

Validación de errores: 400 con detalles consistentes

Si el request DTO usa Bean Validation (por ejemplo @NotBlank, @Min), prueba que el API responde con 400 y un cuerpo estable. Para evitar tests frágiles, aserta campos clave en lugar del mensaje exacto (que puede variar por locale).

@Test void returns400_whenQuantityInvalid() throws Exception {  mvc.perform(post("/orders")      .contentType("application/json")      .content("{\"customerId\":\"customer-1\",\"sku\":\"SKU-1\",\"quantity\":0}"))    .andExpect(status().isBadRequest())    .andExpect(jsonPath("$.error").value("validation_error"))    .andExpect(jsonPath("$.fields.quantity").exists());}

Para que esto funcione, normalmente tendrás un @ControllerAdvice que transforme MethodArgumentNotValidException a un formato de error estable. La prueba del controlador verifica el contrato de errores del microservicio.

4) Pruebas de contrato para APIs: productor y consumidor

Las pruebas de contrato reducen roturas entre microservicios al formalizar expectativas. En lugar de depender de pruebas E2E, el consumidor define (o acuerda) un contrato, y el productor lo verifica automáticamente. Dos enfoques comunes:

  • Consumer-driven contracts: el consumidor define el contrato; el productor lo valida.
  • Contratos compartidos: ambos equipos acuerdan un repositorio/artefacto versionado.

Ejemplo conceptual con Spring Cloud Contract (productor)

Un contrato describe request/response. Spring Cloud Contract puede generar tests del lado productor. Ejemplo de contrato (Groovy/YAML según configuración):

// src/test/resources/contracts/orders/shouldReturnOrder.groovy Contract.make {  request {    method 'GET'    urlPath('/orders/order-1')  }  response {    status 200    headers {      contentType(applicationJson())    }    body(      id: 'order-1',      customerId: 'customer-1',      sku: 'SKU-1',      quantity: 2    )  }}

Al ejecutar tests, se generan verificaciones que fallan si el endpoint cambia. Además, se puede publicar un stub para que el consumidor lo use en sus pruebas sin levantar el productor real.

Consumidor: pruebas contra stub determinista

El consumidor puede ejecutar pruebas de integración contra un stub local (generado desde el contrato). Esto elimina flakiness por red y disponibilidad del productor, manteniendo realismo en el payload.

5) Simular dependencias externas sin flakiness

Patrones recomendados

  • Mock en unitarias: simula clientes HTTP/colas con Mockito y aserta interacciones.
  • Stub HTTP en integración: usa un servidor stub (por ejemplo WireMock) para respuestas realistas y deterministas.
  • Testcontainers para infraestructura: base de datos, broker, etc., cuando el comportamiento real importa.
  • Evita sleeps: usa timeouts y esperas activas con límites (si hay asincronía), o mejor, diseña para testear sin temporizadores.

Ejemplo: stub HTTP con WireMock (integración)

Si tu microservicio llama a un servicio de inventario, puedes stubearlo:

@SpringBootTest @AutoConfigureWireMock(port = 0) class InventoryClientIT {  @Autowired InventoryClient client;  @DynamicPropertySource  static void props(DynamicPropertyRegistry r) {    r.add("inventory.base-url", () -> "http://localhost:" + System.getProperty("wiremock.server.port"));  }  @Test  void returnsTrue_whenStubSaysInStock() {    stubFor(get(urlEqualTo("/inventory/SKU-1?qty=2"))      .willReturn(aResponse()        .withHeader("Content-Type", "application/json")        .withBody("{\"available\":true}")));    assertTrue(client.hasStock("SKU-1", 2));  }}

Determinismo: el stub devuelve siempre el mismo body para el mismo request. Evita depender de timestamps o IDs generados aleatoriamente en el stub.

6) Guía práctica paso a paso para montar la estrategia

Paso 1: clasifica tests por tipo

  • *Test para unitarias (sin Spring).
  • *IT para integración (Spring + DB/HTTP stub).
  • *ContractTest o carpeta contracts/ para contratos.

Paso 2: asegura datos y tiempo deterministas

  • Inyecta Clock y usa Clock.fixed en tests.
  • Evita UUID.randomUUID() en aserciones; inyecta generador.
  • Usa fixtures explícitas (builders) en lugar de datos aleatorios.

Paso 3: define un formato de error estable

Estándar interno: { error, message, fields, traceId } (o similar). Prueba al menos:

  • 400 por validación.
  • 404 por recurso inexistente.
  • 409 por conflicto (por ejemplo, duplicados).

Paso 4: integra Testcontainers donde aporte valor

  • Persistencia: PostgreSQL/MySQL reales.
  • Si hay mensajería: contenedor del broker (cuando sea crítico).

Paso 5: añade contratos para endpoints críticos

  • Endpoints consumidos por otros microservicios.
  • Errores y códigos HTTP acordados.
  • Headers relevantes (auth, idempotency, versionado).

7) Criterios de cobertura útiles (más allá del porcentaje)

CriterioQué buscarDónde
Ramas de negocioCasos válidos e inválidos, límites, reglasUnitarias
Contrato de APIPayloads, códigos, errores, headersWeb + Contrato
Persistencia realConstraints, queries, transacciones relevantesIntegración (DB)
Resiliencia ante fallos externosTimeouts, respuestas erróneas, degradaciónIntegración con stubs
No flakiness0 dependencia de orden, tiempo, red inestableTodos

Como referencia práctica, suele ser más valioso exigir cobertura alta en dominio/casos de uso (donde vive la lógica) que perseguir 100% en controladores triviales. En web, prioriza pruebas de rutas, validación y formato de errores.

8) Integración en un pipeline de entrega

Separar fases por costo

Una estrategia común en CI:

  • Fase 1 (rápida): unitarias + lint/format (segundos).
  • Fase 2: integración web/persistencia con Testcontainers (minutos).
  • Fase 3: verificación de contratos y publicación de stubs (si aplica).

Ejemplo de configuración (conceptual) con perfiles

Puedes etiquetar tests con JUnit 5 @Tag para ejecutarlos por etapa:

@Tag("unit") class OrderServiceTest { ... } @Tag("integration") class OrderRepositoryIT { ... }

Y en CI ejecutar:

# etapa rápida ./gradlew test -Dgroups=unit # o usando tags según tu build tool # etapa integración ./gradlew test -Dgroups=integration

En pipelines, habilita cache de dependencias, ejecuta tests en paralelo cuando sea posible, y publica reportes (JUnit XML, cobertura) como artefactos. Si usas contratos con stubs, versiona y publica el stub en un repositorio de artefactos para que los consumidores lo consuman en sus propias pipelines.

Ahora responde el ejercicio sobre el contenido:

¿Cuál estrategia reduce el costo y la fragilidad de las pruebas automatizadas en microservicios y ayuda a diagnosticar rápidamente qué se rompió y dónde?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La pirámide busca rapidez y determinismo: muchas unitarias sin infraestructura, algunas de integración para persistencia/web y pocas pruebas de contrato para compatibilidad entre servicios. Así, los fallos son menos frágiles y señalan mejor qué y dónde se rompió.

Siguiente capítulo

Despliegue de microservicios Spring Boot y ejecución en contenedores

Arrow Right Icon
Portada de libro electrónico gratuitaMicroservicios con Spring Boot desde Cero
75%

Microservicios con Spring Boot desde Cero

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.