Loading skill
Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 191% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 212% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 317% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 1306% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 373% | 0% |
80%+ kapsam (unit + integration) ile Quarkus 3.x servisleri için TDD rehberi. Apache Camel ile event-driven mimariler için optimize edilmiştir.
Kapsamlı ve okunabilir testler için bu yapılandırılmış yaklaşımı izleyin:
java@ExtendWith(MockitoExtension.class) @DisplayName("As2ProcessingService Unit Tests") class As2ProcessingServiceTest { @Mock private InvoiceFlowValidator invoiceFlowValidator; @Mock private EventService eventService; @Mock private DocumentJobService documentJobService; @Mock private BusinessRulesPublisher businessRulesPublisher; @Mock private FileStorageService fileStorageService; @InjectMocks private As2ProcessingService as2ProcessingService; private Path testFilePath; private LogContext testLogContext; private InvoiceValidationResult validationResult; private StoredDocumentInfo documentInfo; @BeforeEach void setUp() { // ARRANGE - Ortak test verisi testFilePath = Path.of("/tmp/test-invoice.xml"); testLogContext = new LogContext(); testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001"); testLogContext.put(As2Constants.FILE_NAME, "invoice.xml"); testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001"); validationResult = new InvoiceValidationResult(); validationResult.setValid(true); validationResult.setSize(1024L); validationResult.setDocumentHash("abc123"); documentInfo = new StoredDocumentInfo(); documentInfo.setPath("s3://bucket/path/invoice.xml"); documentInfo.setSize(1024L); } @Nested @DisplayName("processFile için testler") class ProcessFile { @Test @DisplayName("CHORUS olmayan dosyayı tüm validasyonlarla başarıyla işlemeli") void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception { // ARRANGE testLogContext.put(As2Constants.CHORUS_FLOW, "false"); CustomLog.setCurrentContext(testLogContext); when(invoiceFlowValidator.validateFlowWithConfig( eq(testFilePath), eq(ValidationFlowConfig.allValidations()), eq(EInvoiceSyntaxFormat.UBL), any(LogContext.class))) .thenReturn(validationResult); when(invoiceFlowValidator.computeFlowProfile(any(), any())) .thenReturn(FlowProfile.BASIC); when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) .thenReturn(CompletableFuture.completedFuture(documentInfo)); when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any())) .thenReturn(new BusinessRulesPayload()); // ACT assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); // ASSERT verify(invoiceFlowValidator).validateFlowWithConfig( eq(testFilePath), eq(ValidationFlowConfig.allValidations()), eq(EInvoiceSyntaxFormat.UBL), any(LogContext.class)); verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class), eq("PERSISTENCE_BLOB_EVENT_TYPE")); verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class), eq("BUSINESS_RULES_MESSAGE_SENT")); verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class)); } @Test @DisplayName("CHORUS_FLOW için schematron validasyonu atlanmalı") void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception { // ARRANGE testLogContext.put(As2Constants.CHORUS_FLOW, "true"); CustomLog.setCurrentContext(testLogContext); when(invoiceFlowValidator.validateFlowWithConfig( eq(testFilePath), eq(ValidationFlowConfig.xsdOnly()), eq(EInvoiceSyntaxFormat.UBL), any(LogContext.class))) .thenReturn(validationResult); when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) .thenReturn(CompletableFuture.completedFuture(documentInfo)); when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), eq(FlowProfile.EXTENDED_CTC_FR), any())) .thenReturn(new BusinessRulesPayload()); // ACT assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); // ASSERT verify(invoiceFlowValidator).validateFlowWithConfig( eq(testFilePath), eq(ValidationFlowConfig.xsdOnly()), eq(EInvoiceSyntaxFormat.UBL), any(LogContext.class)); verify(documentJobService).createDocumentAndJobEntities( any(), any(), any(), eq(FlowProfile.EXTENDED_CTC_FR), any()); } @Test @DisplayName("Dosya yükleme başarısız olduğunda hata eventi oluşturulmalı") void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception { // ARRANGE testLogContext.put(As2Constants.CHORUS_FLOW, "false"); CustomLog.setCurrentContext(testLogContext); when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) .thenReturn(validationResult); when(invoiceFlowValidator.computeFlowProfile(any(), any())) .thenReturn(FlowProfile.BASIC); documentInfo.setPath(""); // Boş path hatayı tetikler when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) .thenReturn(CompletableFuture.completedFuture(documentInfo)); // ACT & ASSERT As2ServerProcessingException exception = assertThrows( As2ServerProcessingException.class, () -> as2ProcessingService.processFile(testFilePath) ); assertThat(exception.getMessage()) .contains("File path is empty after upload"); verify(eventService).createErrorEvent( eq(documentInfo), eq("FILE_UPLOAD_FAILED"), contains("File path is empty")); verify(businessRulesPublisher, never()).publishAsync(any()); } @Test @DisplayName("CompletableFuture.join() başarısızlığı ele alınmalı") void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception { // ARRANGE testLogContext.put(As2Constants.CHORUS_FLOW, "false"); CustomLog.setCurrentContext(testLogContext); when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) .thenReturn(validationResult); when(invoiceFlowValidator.computeFlowProfile(any(), any())) .thenReturn(FlowProfile.BASIC); CompletableFuture<StoredDocumentInfo> failedFuture = CompletableFuture.failedFuture(new StorageException("S3 connection failed")); when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) .thenReturn(failedFuture); // ACT & ASSERT assertThrows( CompletionException.class, () -> as2ProcessingService.processFile(testFilePath) ); } @Test @DisplayName("Dosya yolu null olduğunda exception fırlatılmalı") void givenNullFilePath_whenProcessFile_thenThrowsException() { // ARRANGE Path nullPath = null; // ACT & ASSERT NullPointerException exception = assertThrows( NullPointerException.class, () -> as2ProcessingService.processFile(nullPath) ); verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any()); } } }
givenX_whenY_thenZ// ARRANGE, // ACT, // ASSERT yorumlarıverify() kullanınnever() kullanınjava@QuarkusTest @DisplayName("Business Rules Camel Route Tests") class BusinessRulesRouteTest { @Inject CamelContext camelContext; @Inject ProducerTemplate producerTemplate; @InjectMock EventService eventService; private BusinessRulesPayload testPayload; @BeforeEach void setUp() { // ARRANGE - Test verisi testPayload = new BusinessRulesPayload(); testPayload.setDocumentId(1L); testPayload.setFlowProfile(FlowProfile.BASIC); } @Nested @DisplayName("business-rules-publisher route için testler") class BusinessRulesPublisher { @Test @DisplayName("Mesajı başarıyla RabbitMQ'ya yayınlamalı") void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception { // ARRANGE MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); mockRabbitMQ.expectedMessageCount(1); // Test için gerçek endpoint'i mock ile değiştir 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 — .marshal().json() sonrası body JSON String'dir mockRabbitMQ.assertIsSatisfied(5000); assertThat(mockRabbitMQ.getExchanges()).hasSize(1); String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); } @Test @DisplayName("JSON'a marshalling'i ele almalı") void givenPayload_whenPublish_thenMarshalledToJson() throws Exception { // ARRANGE MockEndpoint mockMarshal = new MockEndpoint("mock:marshal"); camelContext.addEndpoint("mock:marshal", mockMarshal); mockMarshal.expectedMessageCount(1); camelContext.getRouteController().stopRoute("business-rules-publisher"); AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { advice.weaveAddLast().to("mock:marshal"); }); camelContext.getRouteController().startRoute("business-rules-publisher"); // ACT producerTemplate.sendBody("direct:business-rules-publisher", testPayload); // ASSERT mockMarshal.assertIsSatisfied(5000); String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); assertThat(body).contains("\"flowProfile\":\"BASIC\""); } } @Nested @DisplayName("document-processing route için testler") class DocumentProcessing { @Test @DisplayName("Faturayı doğru işlemciye yönlendirmeli") void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception { // ARRANGE MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class); mockInvoice.expectedMessageCount(1); camelContext.getRouteController().stopRoute("document-processing"); AdviceWith.adviceWith(camelContext, "document-processing", advice -> { advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice"); }); camelContext.getRouteController().startRoute("document-processing"); // ACT producerTemplate.sendBodyAndHeader("direct:process-document", testPayload, "documentType", "INVOICE"); // ASSERT mockInvoice.assertIsSatisfied(5000); } @Test @DisplayName("Validasyon hatalarını zarif şekilde ele almalı") void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception { // ARRANGE MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); mockError.expectedMessageCount(1); camelContext.getRouteController().stopRoute("document-processing"); AdviceWith.adviceWith(camelContext, "document-processing", advice -> { advice.weaveByToString(".*direct:validation-error-handler.*") .replace().to("mock:error"); }); camelContext.getRouteController().startRoute("document-processing"); // Error event oluşturma hatasını gerçek EventService API'si üzerinden simüle et doThrow(new ValidationException("Invalid document")) .when(eventService) .createErrorEvent(any(), eq("VALIDATION_ERROR"), anyString()); // ACT producerTemplate.sendBody("direct:process-document", testPayload); // ASSERT mockError.assertIsSatisfied(5000); Exception exception = mockError.getExchanges().get(0).getException(); assertThat(exception).isInstanceOf(ValidationException.class); assertThat(exception.getMessage()).contains("Invalid document"); } } }
java@ExtendWith(MockitoExtension.class) @DisplayName("EventService Unit Tests") class EventServiceTest { @Mock private EventRepository eventRepository; @Mock private ObjectMapper objectMapper; @InjectMocks private EventService eventService; private BusinessRulesPayload testPayload; @BeforeEach void setUp() { // ARRANGE testPayload = new BusinessRulesPayload(); testPayload.setDocumentId(1L); } @Nested @DisplayName("createSuccessEvent için testler") class CreateSuccessEvent { @Test @DisplayName("Doğru niteliklerle başarı eventi oluşturulmalı") void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception { // ARRANGE 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.getPayload().equals("{\"documentId\":1}") && event.getTimestamp() != null )); } @Test @DisplayName("Payload null olduğunda exception fırlatılmalı") 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("createErrorEvent için testler") class CreateErrorEvent { @Test @DisplayName("Hata mesajıyla hata eventi oluşturulmalı") void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception { // ARRANGE String errorMessage = "Processing failed"; when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); // ACT assertDoesNotThrow(() -> eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage)); // ASSERT verify(eventRepository).persist(argThat(event -> event.getType().equals("PROCESSING_ERROR") && event.getStatus() == EventStatus.ERROR && event.getErrorMessage().equals(errorMessage) && event.getPayload().equals("{\"documentId\":1}") )); } @ParameterizedTest @DisplayName("Geçersiz hata mesajları reddedilmeli") @ValueSource(strings = {"", " "}) void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) { // ACT & ASSERT IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage) ); assertThat(exception.getMessage()).contains("Error message cannot be blank"); } } }
java@ExtendWith(MockitoExtension.class) @DisplayName("FileStorageService Unit Tests") class FileStorageServiceTest { @Mock private S3Client s3Client; @Mock private ExecutorService executorService; @InjectMocks private FileStorageService fileStorageService; private InputStream testInputStream; private LogContext testLogContext; @BeforeEach void setUp() { // ARRANGE testInputStream = new ByteArrayInputStream("test content".getBytes()); testLogContext = new LogContext(); testLogContext.put("traceId", "trace-123"); } @Nested @DisplayName("uploadOriginalFile için testler") class UploadOriginalFile { @Test @DisplayName("Dosyayı başarıyla yüklemeli ve belge bilgisi döndürmeli") void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception { // ARRANGE doAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenReturn(PutObjectResponse.builder().build()); // ACT CompletableFuture<StoredDocumentInfo> future = fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); StoredDocumentInfo result = future.join(); // ASSERT assertThat(result).isNotNull(); assertThat(result.getPath()).isNotBlank(); assertThat(result.getSize()).isEqualTo(1024L); assertThat(result.getUploadedAt()).isNotNull(); verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); } @Test @DisplayName("S3 yükleme başarısızlığını ele almalı") void givenS3Failure_whenUpload_thenCompletableFutureFails() { // ARRANGE 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 unavailable")); // ACT CompletableFuture<StoredDocumentInfo> future = fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); // ASSERT assertThatThrownBy(() -> future.join()) .isInstanceOf(CompletionException.class) .hasCauseInstanceOf(StorageException.class) .hasMessageContaining("S3 unavailable"); } @Test @DisplayName("LogContext'i async işleme yaymalı") void givenLogContext_whenUpload_thenContextPropagated() throws Exception { // ARRANGE AtomicReference<LogContext> capturedContext = new AtomicReference<>(); doAnswer(invocation -> { capturedContext.set(CustomLog.getCurrentContext()); ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); // ACT fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL).join(); // ASSERT assertThat(capturedContext.get()).isNotNull(); assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123"); } } }
java@QuarkusTest @DisplayName("DocumentResource API Tests") class DocumentResourceTest { @InjectMock DocumentService documentService; @Nested @DisplayName("GET /api/documents için testler") class ListDocuments { @Test @DisplayName("Belge listesini döndürmeli") void givenDocumentsExist_whenList_thenReturnsOk() { // ARRANGE List<Document> documents = List.of(createDocument(1L, "DOC-001")); when(documentService.list(0, 20)).thenReturn(documents); // ACT & ASSERT given() .when().get("/api/documents") .then() .statusCode(200) .body("$.size()", is(1)) .body("[0].referenceNumber", equalTo("DOC-001")); } } @Nested @DisplayName("POST /api/documents için testler") class CreateDocument { @Test @DisplayName("Belge oluşturmalı ve 201 döndürmeli") 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": "Test document", "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("Geçersiz girdi için 400 döndürmeli") void givenInvalidRequest_whenCreate_thenReturns400() { // ACT & ASSERT given() .contentType(ContentType.JSON) .body(""" { "referenceNumber": "", "description": "Test" } """) .when().post("/api/documents") .then() .statusCode(400); } } private Document createDocument(Long id, String referenceNumber) { Document document = new Document(); document.setId(id); document.setReferenceNumber(referenceNumber); document.setStatus(DocumentStatus.PENDING); return document; } }
java@QuarkusTest @TestProfile(IntegrationTestProfile.class) @DisplayName("Document Integration Tests") class DocumentIntegrationTest { @Test @Transactional @DisplayName("Belge oluşturulmalı ve API üzerinden alınabilmeli") void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() { // ACT - API üzerinden oluştur Long id = given() .contentType(ContentType.JSON) .body(""" { "referenceNumber": "INT-001", "description": "Integration test", "validUntil": "2030-01-01T00:00:00Z", "categories": ["test"] } """) .when().post("/api/documents") .then() .statusCode(201) .extract().path("id"); // ASSERT - API üzerinden al given() .when().get("/api/documents/" + id) .then() .statusCode(200) .body("referenceNumber", equalTo("INT-001")); } }
xml<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.13</version> <executions> <!-- Test yürütmesi için agent'ı hazırla --> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <!-- Kapsam raporu oluştur --> <execution> <id>report</id> <phase>verify</phase> <goals> <goal>report</goal> </goals> </execution> <!-- Kapsam eşiklerini zorla --> <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>
Kapsam ile testleri çalıştırın:
bashmvn clean test mvn jacoco:report mvn jacoco:check # Rapor: target/site/jacoco/index.html
xml<dependencies> <!-- Quarkus Test --> <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> <!-- Mockito --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <scope>test</scope> </dependency> <!-- AssertJ (JUnit assertion'larına tercih edilir) --> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.24.2</version> <scope>test</scope> </dependency> <!-- REST Assured --> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> <!-- Camel Test --> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-junit5</artifactId> <scope>test</scope> </dependency> </dependencies>
@Nested sınıflar kullanın@DisplayName kullanıngivenX_whenY_thenZ isimlendirme kuralını izleyin@BeforeEach kullanın// ARRANGE, // ACT, // ASSERT)assertDoesNotThrow kullanınassertThrows kullanıncontains() veya isEqualTo() kullanarak exception mesajlarının beklenen değerlerle eşleştiğini doğrulayınassertThat)assertThat(list).hasSize(3).contains(item)assertThrows ile yakalayın, ardından AssertJ ile mesajı doğrulayınassertDoesNotThrow kullanınextracting(), filteredOn(), containsExactly()@QuarkusTest kullanın@InjectMock kullanın@TestProfile kullanınAdviceWith ve MockEndpoint ile Camel route'larını test edin@CamelQuarkusTest annotasyonu kullanın (bağımsız Camel testleri kullanıyorsanız)MockEndpoint kullanınAdviceWith kullanın (endpoint'leri mock'larla değiştirin).join() kullanınmvn quarkus:test@ParameterizedTest) kullanın@MockBean yerine @InjectMock kullanın (Quarkus'a özgü)verify(mock, never()) kullanınargThat() kullanınInOrderOther measured skills in the registry, with their headline benchmark lift.