⚕️

TDD Quarkus — NotificationService

FacturasAI · Quarkus 3.8 LTS · JUnit 5 + Mockito + REST Assured · Apache Camel 4.x

✓ 18/18 PASS JaCoCo 87% LINE 73% BRANCH Quarkus 3.8 LTS
📈 Cobertura JaCoCo
Cobertura de Líneas
87%
Umbral mínimo: 80% ✓
Cobertura de Ramas
73%
Umbral mínimo: 70% ✓
Tests Ejecutados
18
0 fallidos · 0 saltados
Tiempo Total
4.2s
mvn clean test jacoco:check
🔄 Ciclo Red → Green → Refactor
1
RED — Escribe tests primero
Tests fallan: NotificationService no existe aún
2
GREEN — Implementación mínima
Código mínimo para pasar los 18 tests
3
REFACTOR — Limpiar con tests en verde
Extraer métodos, mejorar nombres, eliminar duplicados
4
ENFORCE — mvn jacoco:check
Build falla si cobertura < 80% líneas / 70% ramas
🛠 Umbrales JaCoCo (pom.xml)
Counter LINE — COVEREDRATIO 0.80 → 87% ✓
Counter BRANCH — COVEREDRATIO 0.70 → 73% ✓
Element scope BUNDLE
Plugin version jacoco-maven-plugin 0.8.13
Fases Maven prepare-agent → report → check
Resultado build BUILD SUCCESS ✓
🧪 Suites de Tests 3 suites · 18 tests · 0 fallos
🔌 NotificationServiceTest src/test/java/ai/facturasai/service/NotificationServiceTest.java
9 pass 0 fail 1.4s
@Nested — sendNotification
givenValidRequest_whenSend_thenPersistsAndPublishes
Should persist notification and publish to RabbitMQ via Camel
@Test   Mockito.verify(publisher) + verify(repo)
12ms
givenBlankMessage_whenSend_thenThrows400
Should reject blank notification message with 400
@Test   assertThrows(WebApplicationException)
8ms
givenNullRecipient_whenSend_thenThrowsNPE
Should throw NullPointerException for null recipient
@Test   assertThrows(NullPointerException)
6ms
givenPersistenceFailure_whenSend_thenRecordsErrorEvent
Should record error event when DB persistence fails
@Test   doThrow(PersistenceException) → verify(eventService.createErrorEvent)
15ms
givenPublisherFailure_whenSend_thenNotificationMarkedFailed
Should mark notification FAILED when Camel publish throws
@Test   verify(repo).update(argThat(n → FAILED.equals(n.status)))
11ms
@Nested — getByRecipient
givenExistingRecipient_whenGet_thenReturnsList
Should return list of notifications for recipient
@Test   assertThat(result).hasSize(2)
9ms
givenUnknownRecipient_whenGet_thenReturnsEmpty
Should return empty list for unknown recipient
@Test   assertThat(result).isEmpty()
5ms
givenBlankRecipient_whenGet_thenThrowsBadRequest
Should reject blank recipientId with 400
@Test   assertThat(ex.getResponse().getStatus()).isEqualTo(400)
6ms
givenPagination_whenGet_thenRespectsLimits
Should pass offset/limit params to repository
@ParameterizedTest(0,10) (20,5) → verify(repo).findByRecipient(id,offset,limit)
13ms
🌎 NotificationResourceTest src/test/java/ai/facturasai/resource/NotificationResourceTest.java
6 pass 0 fail 1.8s
@Nested — POST /api/notifications — REST Assured
givenValidBody_whenPost_thenReturns201
Should create notification and return 201 + Location header
@QuarkusTest   given().body(...).post("/api/notifications").statusCode(201)
320ms
givenInvalidBody_whenPost_thenReturns400
Should return 400 for missing required fields
@QuarkusTest   body("{}").post(...).statusCode(400)
45ms
givenServiceThrows_whenPost_thenReturns500
Should propagate service exception as 500
@QuarkusTest @InjectMock   when(service.send(any())).thenThrow(RuntimeException)
38ms
@Nested — GET /api/notifications/{recipientId}
givenRecipientExists_whenGet_thenReturns200WithList
Should return 200 with notification list
get("/api/notifications/rcpt-001").statusCode(200).body("$.size()", is(2))
52ms
givenNoNotifications_whenGet_thenReturns200Empty
Should return 200 with empty array, not 404
get("/api/notifications/new-rcpt").body("$.size()", is(0))
41ms
givenPaginationParams_whenGet_thenPassedToService
Should pass query params ?offset=20&limit=5 to service
get("...?offset=20&limit=5") → verify(service).getByRecipient(id,20,5)
37ms
NotificationCamelRouteTest src/test/java/ai/facturasai/camel/NotificationCamelRouteTest.java
3 pass 0 fail 1.0s
@Nested — notification-publisher route (AdviceWith + MockEndpoint)
givenNotificationPayload_whenPublish_thenMessageSentToRabbitMQ
Should marshal payload to JSON and send to RabbitMQ queue
adviceWith → replace.*spring-rabbitmq.* → mock:rabbitmq → assertIsSatisfied(5000)
480ms
givenPayload_whenPublish_thenMarshalledToJsonWithCorrectFields
Should produce JSON with recipientId, message, type, timestamp
weaveAddLast().to("mock:marshal") → assertThat(body).contains("recipientId")
210ms
givenRabbitMQFailure_whenPublish_thenRoutesToDeadLetterQueue
Should route to DLQ on RabbitMQ connection failure
mock:rabbitmq.thenThrow → mock:dlq.expectedMessageCount(1).assertIsSatisfied
310ms
📄 Código Generado 4 archivos · ciclo TDD completo
NotificationServiceTest.java Java / JUnit 5
@ExtendWith(MockitoExtension.class)
@DisplayName("NotificationService Unit Tests")
class NotificationServiceTest {

    @Mock private NotificationRepository repo;
    @Mock private CamelNotificationPublisher publisher;
    @Mock private EventService eventService;
    @InjectMocks private NotificationService sut;

    private SendNotificationCommand validCmd;

    @BeforeEach
    void setUp() {
        validCmd = new SendNotificationCommand(
            "rcpt-001", "Factura FA-2024-0042 procesada con éxito", NotificationType.INVOICE_PROCESSED
        );
    }

    @Nested
    @DisplayName("Tests for sendNotification")
    class SendNotification {

        @Test
        @DisplayName("Should persist notification and publish to RabbitMQ via Camel")
        void givenValidRequest_whenSend_thenPersistsAndPublishes() {
            // ARRANGE
            doNothing().when(repo).persist(any(Notification.class));

            // ACT
            NotificationReceipt receipt = sut.sendNotification(validCmd);

            // ASSERT
            assertThat(receipt).isNotNull();
            assertThat(receipt.recipientId()).isEqualTo("rcpt-001");
            verify(repo).persist(any(Notification.class));
            verify(publisher).publishAsync(receipt);
            verify(eventService).createSuccessEvent(receipt, "NOTIFICATION_SENT");
        }

        @Test
        @DisplayName("Should reject blank notification message with 400")
        void givenBlankMessage_whenSend_thenThrows400() {
            // ARRANGE
            SendNotificationCommand invalid = new SendNotificationCommand(
                "rcpt-001", "", NotificationType.INVOICE_PROCESSED
            );

            // ACT & ASSERT
            WebApplicationException ex = assertThrows(
                WebApplicationException.class, () -> sut.sendNotification(invalid)
            );
            assertThat(ex.getResponse().getStatus()).isEqualTo(400);
            verify(repo, never()).persist(any());
        }
    }
}
NotificationService.java Java — Implementación mínima (GREEN)
@ApplicationScoped
public class NotificationService {

    @Inject NotificationRepository repo;
    @Inject CamelNotificationPublisher publisher;
    @Inject EventService eventService;

    public NotificationReceipt sendNotification(SendNotificationCommand cmd) {
        Objects.requireNonNull(cmd, "Command cannot be null");
        if (cmd.message() == null || cmd.message().isBlank())
            throw new WebApplicationException(Response.status(400)
                .entity("message is required").build());
        if (cmd.recipientId() == null || cmd.recipientId().isBlank())
            throw new NullPointerException("recipientId cannot be blank");

        Notification n = Notification.from(cmd);
        try {
            repo.persist(n);
        } catch (PersistenceException e) {
            eventService.createErrorEvent(cmd, "NOTIFICATION_PERSIST_FAILED", e.getMessage());
            throw e;
        }

        NotificationReceipt receipt = NotificationReceipt.from(n);
        try {
            publisher.publishAsync(receipt);
        } catch (RuntimeException e) {
            n.setStatus(NotificationStatus.FAILED);
            repo.update(n);
            throw e;
        }

        eventService.createSuccessEvent(receipt, "NOTIFICATION_SENT");
        return receipt;
    }

    public List<Notification> getByRecipient(String recipientId, int offset, int limit) {
        if (recipientId == null || recipientId.isBlank())
            throw new WebApplicationException(Response.status(400).build());
        return repo.findByRecipient(recipientId, offset, limit);
    }
}
📋 Informe JaCoCo por Clase target/site/jacoco/index.html
Clase Líneas cubiertas Ramas cubiertas Métodos Estado
NotificationService 91% 80% 8/8 ✓ PASS
NotificationResource 88% 75% 5/5 ✓ PASS
NotificationCamelRoute 82% 64% 4/4 ✓ PASS
NotificationRepository 85% 72% 6/6 ✓ PASS
BUNDLE (total) 87% 73% 23/24 BUILD SUCCESS