Quarkus Tdd

作者 affaan-mef648e01899b無授權條款275K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫3 天前更新

Desarrollo guiado por pruebas para Quarkus 3.x LTS usando JUnit 5, Mockito, REST Assured, pruebas Camel y JaCoCo. Usar al agregar funcionalidades, corregir bugs o refactorizar servicios orientados a eventos.

AI 產生的概覽

指導 Quarkus 3.x 服務的測試驅動開發,使用 JUnit 5、Mockito、REST Assured、Camel 測試與 JaCoCo。

功能
提供 Quarkus 3.x 服務的 TDD 工作流程:先寫失敗測試、實作最小程式碼、重構,並用 JaCoCo 強制覆蓋率。它提供單元測試(@Nested 類別)、Camel 路由測試、事件服務測試、CompletableFuture 測試與 REST Assured 資源測試的範例模式。也包含 Maven JaCoCo 設定與測試相依性片段。
適用情境
適用於新增功能、修正錯誤或重構 Quarkus 服務時,尤其是使用 Apache Camel 與 RabbitMQ 的事件驅動架構。適合希望取得 Quarkus 測試中 JUnit 5、Mockito、REST Assured 與 JaCoCo 具體範例的團隊。
執行需求
需要一個 Quarkus 3.x 專案,使用 Maven、JUnit 5、Mockito、REST Assured、Camel Quarkus JUnit 5、AssertJ 與 JaCoCo。不附帶指令碼,僅提供說明與程式碼範例。

Flujo de Trabajo TDD en Quarkus

Orientación TDD para servicios Quarkus 3.x con 80%+ de cobertura (unit + integración). Optimizado para arquitecturas orientadas a eventos con Apache Camel.

Cuándo Usar

  • Nuevas funcionalidades o endpoints REST
  • Correcciones de bugs o refactorizaciones
  • Agregar lógica de acceso a datos, reglas de seguridad o streams reactivos
  • Probar rutas Apache Camel y manejadores de eventos
  • Probar servicios orientados a eventos con RabbitMQ
  • Probar lógica de flujo condicional
  • Validar operaciones asíncronas con CompletableFuture
  • Probar propagación de LogContext

Flujo de Trabajo

  1. Escribir pruebas primero (deben fallar)
  2. Implementar el código mínimo para que pasen
  3. Refactorizar con pruebas en verde
  4. Exigir cobertura con JaCoCo (objetivo 80%+)

Pruebas Unitarias con Organización @Nested

java
@ExtendWith(MockitoExtension.class)@DisplayName("Pruebas Unitarias de OrderService")class OrderServiceTest {
  @Mock  private OrderRepository orderRepository;
  @Mock  private EventService eventService;
  @Mock  private FulfillmentPublisher fulfillmentPublisher;
  @InjectMocks  private OrderService orderService;
  private CreateOrderCommand validCommand;
  @BeforeEach  void setUp() {    validCommand = new CreateOrderCommand(        "customer-123",        List.of(new OrderLine("sku-123", 2))    );  }
  @Nested  @DisplayName("Pruebas para createOrder")  class CreateOrder {
    @Test    @DisplayName("Debe persistir orden y publicar evento de fulfillment")    void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {      // ARRANGE      doNothing().when(orderRepository).persist(any(Order.class));
      // ACT      OrderReceipt receipt = orderService.createOrder(validCommand);
      // ASSERT      assertThat(receipt).isNotNull();      assertThat(receipt.customerId()).isEqualTo("customer-123");      verify(orderRepository).persist(any(Order.class));      verify(fulfillmentPublisher).publishAsync(receipt);      verify(eventService).createSuccessEvent(receipt, "ORDER_CREATED");    }
    @Test    @DisplayName("Debe rechazar customer id vacío")    void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {      // ARRANGE      CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());
      // ACT & ASSERT      WebApplicationException exception = assertThrows(          WebApplicationException.class,          () -> orderService.createOrder(invalid)      );
      assertThat(exception.getResponse().getStatus()).isEqualTo(400);      verify(orderRepository, never()).persist(any(Order.class));      verify(fulfillmentPublisher, never()).publishAsync(any());    }
    @Test    @DisplayName("Debe registrar evento de error cuando falla la persistencia")    void givenPersistenceFailure_whenCreateOrder_thenRecordsErrorEvent() {      // ARRANGE      doThrow(new PersistenceException("base de datos no disponible"))          .when(orderRepository).persist(any(Order.class));
      // ACT & ASSERT      PersistenceException exception = assertThrows(          PersistenceException.class,          () -> orderService.createOrder(validCommand)      );
      assertThat(exception.getMessage()).contains("base de datos no disponible");      verify(eventService).createErrorEvent(          eq(validCommand),          eq("ORDER_CREATE_FAILED"),          contains("base de datos no disponible")      );      verify(fulfillmentPublisher, never()).publishAsync(any());    }  }}

Patrones Clave de Prueba

  1. Clases @Nested: Agrupar pruebas por método bajo prueba
  2. @DisplayName: Proporcionar descripciones legibles para reportes
  3. Convención de nombres: givenX_whenY_thenZ para claridad
  4. Patrón AAA: Comentarios explícitos // ARRANGE, // ACT, // ASSERT
  5. @BeforeEach: Configurar datos de prueba comunes para reducir duplicación
  6. assertDoesNotThrow: Probar escenarios exitosos sin capturar excepciones
  7. assertThrows: Probar escenarios de excepción con validación de mensajes
  8. verify(): Asegurar que los métodos sean llamados correctamente
  9. never(): Asegurar que los métodos NO sean llamados en escenarios de error

Pruebas de Rutas Camel

java
@QuarkusTest@DisplayName("Pruebas de Ruta Camel Business Rules")class BusinessRulesRouteTest {
  @Inject  CamelContext camelContext;
  @Inject  ProducerTemplate producerTemplate;
  @InjectMock  EventService eventService;
  @InjectMock  DocumentValidator documentValidator;
  private BusinessRulesPayload testPayload;
  @BeforeEach  void setUp() {    testPayload = new BusinessRulesPayload();    testPayload.setDocumentId(1L);    testPayload.setFlowProfile(FlowProfile.BASIC);  }
  @Nested  @DisplayName("Pruebas para ruta business-rules-publisher")  class BusinessRulesPublisher {
    @Test    @DisplayName("Debe publicar mensaje exitosamente en RabbitMQ")    void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {      // ARRANGE      MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);      mockRabbitMQ.expectedMessageCount(1);
      camelContext.getRouteController().stopRoute("business-rules-publisher");      AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {        advice.replaceFromWith("direct:business-rules-publisher");        advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");      });      camelContext.getRouteController().startRoute("business-rules-publisher");
      // ACT      producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
      // ASSERT      mockRabbitMQ.assertIsSatisfied(5000);
      assertThat(mockRabbitMQ.getExchanges()).hasSize(1);      String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);      assertThat(body).contains("\"documentId\":1");    }  }}

Pruebas de Servicios de Eventos

java
@ExtendWith(MockitoExtension.class)@DisplayName("Pruebas Unitarias de EventService")class EventServiceTest {
  @Mock  private EventRepository eventRepository;
  @Mock  private ObjectMapper objectMapper;
  @InjectMocks  private EventService eventService;
  @Nested  @DisplayName("Pruebas para createSuccessEvent")  class CreateSuccessEvent {
    @Test    @DisplayName("Debe crear evento de éxito con atributos correctos")    void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {      // ARRANGE      BusinessRulesPayload testPayload = new BusinessRulesPayload();      testPayload.setDocumentId(1L);      when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
      // ACT      assertDoesNotThrow(() ->          eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
      // ASSERT      verify(eventRepository).persist(argThat(event ->          event.getType().equals("DOCUMENT_PROCESSED") &&          event.getStatus() == EventStatus.SUCCESS &&          event.getTimestamp() != null      ));    }
    @Test    @DisplayName("Debe lanzar excepción cuando el payload es null")    void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {      // ARRANGE      Object nullPayload = null;
      // ACT & ASSERT      NullPointerException exception = assertThrows(          NullPointerException.class,          () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")      );
      assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");      verify(eventRepository, never()).persist(any());    }  }
  @Nested  @DisplayName("Pruebas para createErrorEvent")  class CreateErrorEvent {
    @ParameterizedTest    @DisplayName("Debe rechazar mensajes de error inválidos")    @ValueSource(strings = {"", " "})    void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {      // ARRANGE      BusinessRulesPayload testPayload = new BusinessRulesPayload();
      // ACT & ASSERT      IllegalArgumentException exception = assertThrows(          IllegalArgumentException.class,          () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)      );
      assertThat(exception.getMessage()).contains("Error message cannot be blank");    }  }}

Pruebas de CompletableFuture

java
@ExtendWith(MockitoExtension.class)class FileStorageServiceTest {
  @Mock  private S3Client s3Client;
  @Mock  private ExecutorService executorService;
  @InjectMocks  private FileStorageService fileStorageService;
  @Test  @DisplayName("Debe manejar fallo de S3")  void givenS3Failure_whenUpload_thenCompletableFutureFails() {    // ARRANGE — ejecutar sincrónicamente para que la excepción se propague    doAnswer(invocation -> {      ((Runnable) invocation.getArgument(0)).run();      return null;    }).when(executorService).execute(any(Runnable.class));
    when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))        .thenThrow(new StorageException("S3 no disponible"));
    // ACT    CompletableFuture<StoredDocumentInfo> future =        fileStorageService.uploadOriginalFile(testInputStream, 1024L,            testLogContext, InvoiceFormat.UBL);
    // ASSERT    assertThatThrownBy(() -> future.join())        .isInstanceOf(CompletionException.class)        .hasCauseInstanceOf(StorageException.class)        .hasMessageContaining("S3 no disponible");  }}

Pruebas de Capa de Recurso (REST Assured)

java
@QuarkusTest@DisplayName("Pruebas de API DocumentResource")class DocumentResourceTest {
  @InjectMock  DocumentService documentService;
  @Test  @DisplayName("Debe crear documento y retornar 201")  void givenValidRequest_whenCreate_thenReturns201() {    // ARRANGE    Document document = createDocument(1L, "DOC-001");    when(documentService.create(any())).thenReturn(document);
    // ACT & ASSERT    given()        .contentType(ContentType.JSON)        .body("""            {              "referenceNumber": "DOC-001",              "description": "Documento de prueba",              "validUntil": "2030-01-01T00:00:00Z",              "categories": ["test"]            }            """)        .when().post("/api/documents")        .then()        .statusCode(201)        .header("Location", containsString("/api/documents/1"))        .body("referenceNumber", equalTo("DOC-001"));  }
  @Test  @DisplayName("Debe retornar 400 para entrada inválida")  void givenInvalidRequest_whenCreate_thenReturns400() {    given()        .contentType(ContentType.JSON)        .body("""            {              "referenceNumber": "",              "description": "Test"            }            """)        .when().post("/api/documents")        .then()        .statusCode(400);  }}

Cobertura con JaCoCo

Configuración Maven (Completa)

xml
<plugin>  <groupId>org.jacoco</groupId>  <artifactId>jacoco-maven-plugin</artifactId>  <version>0.8.13</version>  <executions>    <execution>      <id>prepare-agent</id>      <goals>        <goal>prepare-agent</goal>      </goals>    </execution>    <execution>      <id>report</id>      <phase>verify</phase>      <goals>        <goal>report</goal>      </goals>    </execution>    <execution>      <id>check</id>      <goals>        <goal>check</goal>      </goals>      <configuration>        <rules>          <rule>            <element>BUNDLE</element>            <limits>              <limit>                <counter>LINE</counter>                <value>COVEREDRATIO</value>                <minimum>0.80</minimum>              </limit>              <limit>                <counter>BRANCH</counter>                <value>COVEREDRATIO</value>                <minimum>0.70</minimum>              </limit>            </limits>          </rule>        </rules>      </configuration>    </execution>  </executions></plugin>

Ejecutar pruebas con cobertura:

bash
mvn clean testmvn jacoco:reportmvn jacoco:check
# Reporte en: target/site/jacoco/index.html

Dependencias de Prueba

xml
<dependencies>    <dependency>        <groupId>io.quarkus</groupId>        <artifactId>quarkus-junit5</artifactId>        <scope>test</scope>    </dependency>    <dependency>        <groupId>io.quarkus</groupId>        <artifactId>quarkus-junit5-mockito</artifactId>        <scope>test</scope>    </dependency>    <dependency>        <groupId>org.assertj</groupId>        <artifactId>assertj-core</artifactId>        <version>3.24.2</version>        <scope>test</scope>    </dependency>    <dependency>        <groupId>io.rest-assured</groupId>        <artifactId>rest-assured</artifactId>        <scope>test</scope>    </dependency>    <dependency>        <groupId>org.apache.camel.quarkus</groupId>        <artifactId>camel-quarkus-junit5</artifactId>        <scope>test</scope>    </dependency></dependencies>

Buenas Prácticas

Organización de Pruebas

  • Usar clases @Nested para agrupar pruebas por método bajo prueba
  • Usar @DisplayName para descripciones legibles en reportes
  • Seguir la convención de nombres givenX_whenY_thenZ
  • Usar @BeforeEach para configuración de datos comunes

Cobertura de Pruebas

  • Probar rutas felices para todos los métodos públicos
  • Probar manejo de entradas null
  • Probar casos borde (colecciones vacías, valores de frontera)
  • Probar escenarios de excepción de forma comprensiva
  • Apuntar a 80%+ de cobertura de líneas, 70%+ de ramas

Aserciones

  • Preferir AssertJ (assertThat) sobre aserciones JUnit para verificar valores
  • Para excepciones: usar JUnit assertThrows para capturar, luego AssertJ para validar
  • Para escenarios exitosos sin excepción: usar JUnit assertDoesNotThrow

Pruebas de Integración

  • Usar @QuarkusTest para pruebas de integración
  • Usar @InjectMock para mockear dependencias en pruebas Quarkus
  • Preferir REST Assured para pruebas de API
  • Usar @TestProfile para configuración específica de prueba

來源與署名

來源:affaan-m/ecc位於docs/es/skills/quarkus-tdd提交ef648e0

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架