--- name: quarkus-tdd description: Test-driven development for Quarkus 3.x LTS using JUnit 5, Mockito, REST Assured, Camel testing, and JaCoCo. Use when adding features, fixing bugs, or refactoring event-driven services. origin: ECC --- # Quarkus TDD İş Akışı 80%+ kapsam (unit + integration) ile Quarkus 3.x servisleri için TDD rehberi. Apache Camel ile event-driven mimariler için optimize edilmiştir. ## When to Use - Yeni özellikler veya REST endpoint'leri - Bug düzeltmeleri veya refactoring'ler - Veri erişim mantığı, güvenlik kuralları veya reaktif akışlar ekleme - Apache Camel route'larını ve event handler'larını test etme - RabbitMQ ile event-driven servisleri test etme - Koşullu akış mantığını test etme - CompletableFuture async işlemlerini doğrulama - LogContext yayılımını test etme ## How It Works 1. Önce testleri yazın (başarısız olmalılar) 2. Geçmek için minimal kod uygulayın 3. Testleri yeşil tutarken refactor edin 4. JaCoCo ile kapsamı zorlayın (%80+ hedef) ## Examples ### @Nested Organizasyonlu Unit Testler 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 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()); } } } ``` ### Temel Test Desenleri 1. **@Nested Sınıflar**: Testleri test edilen metoda göre gruplandırın 2. **@DisplayName**: Test raporlarında okunabilir açıklamalar sağlayın 3. **İsimlendirme Kuralı**: Netlik için `givenX_whenY_thenZ` 4. **AAA Deseni**: Açık `// ARRANGE`, `// ACT`, `// ASSERT` yorumları 5. **@BeforeEach**: Tekrarı azaltmak için ortak test verisi kurulumu 6. **assertDoesNotThrow**: Exception yakalamadan başarı senaryolarını test edin 7. **assertThrows**: AssertJ kullanarak mesaj doğrulamalı exception senaryolarını test edin 8. **Kapsamlı Kapsam**: Mutlu yolları, null girdileri, edge case'leri, exception'ları test edin 9. **Etkileşimleri Doğrulama**: Metodların doğru çağrıldığından emin olmak için Mockito `verify()` kullanın 10. **Hiçbir Zaman Doğrulama**: Hata senaryolarında metodların ÇAĞRILMADIĞINI sağlamak için `never()` kullanın ## Camel Route Testi ```java @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"); } } } ``` ## Event Service Testi ```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"); } } } ``` ## CompletableFuture Testi ```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 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 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 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"); } } } ``` ## Resource Katmanı Testleri (REST Assured) ```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 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; } } ``` ## Gerçek Veritabanıyla Entegrasyon Testleri ```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")); } } ``` ## JaCoCo ile Kapsam ### Maven Yapılandırması (Tam) ```xml org.jacoco jacoco-maven-plugin 0.8.13 prepare-agent prepare-agent report verify report check check BUNDLE LINE COVEREDRATIO 0.80 BRANCH COVEREDRATIO 0.70 ``` Kapsam ile testleri çalıştırın: ```bash mvn clean test mvn jacoco:report mvn jacoco:check # Rapor: target/site/jacoco/index.html ``` ## Test Bağımlılıkları ```xml io.quarkus quarkus-junit5 test io.quarkus quarkus-junit5-mockito test org.mockito mockito-core test org.assertj assertj-core 3.24.2 test io.rest-assured rest-assured test org.apache.camel.quarkus camel-quarkus-junit5 test ``` ## En İyi Uygulamalar ### Test Organizasyonu - Testleri test edilen metoda göre gruplandırmak için `@Nested` sınıflar kullanın - Raporlarda görünür okunabilir açıklamalar için `@DisplayName` kullanın - Test metodları için `givenX_whenY_thenZ` isimlendirme kuralını izleyin - Tekrarı azaltmak için ortak test verisi kurulumunda `@BeforeEach` kullanın ### Test Yapısı - Açık yorumlarla AAA desenini izleyin (`// ARRANGE`, `// ACT`, `// ASSERT`) - Başarı senaryoları için `assertDoesNotThrow` kullanın - Mesaj doğrulamalı exception senaryoları için `assertThrows` kullanın - AssertJ `contains()` veya `isEqualTo()` kullanarak exception mesajlarının beklenen değerlerle eşleştiğini doğrulayın ### Test Kapsamı - Tüm public metodlar için mutlu yolları test edin - Null girdi işlemeyi test edin - Edge case'leri test edin (boş koleksiyonlar, sınır değerleri, negatif ID'ler, boş string'ler) - Exception senaryolarını kapsamlı biçimde test edin - Tüm harici bağımlılıkları mock'layın (repository'ler, servisler, Camel endpoint'leri) - %80+ satır kapsamı, %70+ branch kapsamı hedefleyin ### Assertion'lar - Değer kontrolleri için JUnit assertion'ları yerine **AssertJ'yi tercih edin** (`assertThat`) - Okunabilirlik için akıcı AssertJ API'si kullanın: `assertThat(list).hasSize(3).contains(item)` - Exception'lar için: JUnit `assertThrows` ile yakalayın, ardından AssertJ ile mesajı doğrulayın - Fırlatılmayan başarı yolları için: JUnit `assertDoesNotThrow` kullanın - Koleksiyonlar için: `extracting()`, `filteredOn()`, `containsExactly()` ### Entegrasyon Testi - Entegrasyon testleri için `@QuarkusTest` kullanın - Quarkus testlerinde bağımlılıkları mock'lamak için `@InjectMock` kullanın - API testi için REST Assured'ı tercih edin - Test'e özel yapılandırma için `@TestProfile` kullanın ### Event-Driven Test - `AdviceWith` ve `MockEndpoint` ile Camel route'larını test edin - `@CamelQuarkusTest` annotasyonu kullanın (bağımsız Camel testleri kullanıyorsanız) - Mesaj içeriğini, başlıklarını ve yönlendirme mantığını doğrulayın - Hata işleme route'larını ayrı ayrı test edin - Unit testlerde harici sistemleri (RabbitMQ, S3, veritabanları) mock'layın ### Camel Route Testi - Mesaj akışını doğrulamak için `MockEndpoint` kullanın - Test için route'ları değiştirmek üzere `AdviceWith` kullanın (endpoint'leri mock'larla değiştirin) - Mesaj dönüşümünü ve marshalling'i test edin - Exception işleme ve dead letter queue'ları test edin ### Async İşlem Testi - CompletableFuture başarı ve başarısızlık senaryolarını test edin - Async tamamlanmayı beklemek için testlerde `.join()` kullanın - CompletableFuture'dan exception yayılımını test edin - LogContext yayılımını async işlemlere doğrulayın ### Performans - Testleri hızlı ve izole tutun - Testleri sürekli modda çalıştırın: `mvn quarkus:test` - Girdi varyasyonları için parametreli testler (`@ParameterizedTest`) kullanın - Yeniden kullanılabilir test verisi builder'ları veya factory metodları oluşturun ### Quarkus'a Özgü - En son LTS sürümünde kalın (Quarkus 3.x) - Native derleme uyumluluğunu periyodik olarak test edin - Farklı senaryolar için Quarkus test profillerini kullanın - Yerel test için Quarkus dev servislerinden yararlanın - `@MockBean` yerine `@InjectMock` kullanın (Quarkus'a özgü) ### Doğrulama En İyi Uygulamaları - Mock'lanmış bağımlılıklardaki etkileşimleri her zaman doğrulayın - Hata senaryolarında metodların ÇAĞRILMADIĞINI sağlamak için `verify(mock, never())` kullanın - Karmaşık argüman eşleştirme için `argThat()` kullanın - Önem taşıdığında çağrı sırasını doğrulayın: Mockito'dan `InOrder`