[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6JDEDZPV9NQSHTCGED61S46":3,"skill-same:01M6JDEDZPV9NQSHTCGED61S46":71},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":25,"publishedAt":43,"linkOnly":44,"markdownContent":45,"markdownBody":46,"frontMatter":47,"refName":49,"commitSha":50,"skillDirectory":51,"forksCount":52,"repositoryHtmlUrl":53,"repositoryHomepageUrl":54,"ownerHtmlUrl":55,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":56,"install":57,"resources":70},"01M6JDEDZPV9NQSHTCGED61S46","quarkus-tdd",null,"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.","S3YED\u002Fappie-kit","S3YED","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F62743867?v=4","01M6JBKBRRVP6KMS0VWZ27T3R6","01M6JBKBX0KG30284PW1DBYJT1",14,"MIT","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fecc\u002Fquarkus-tdd\u002FSKILL.md",[17,22],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000023","Skill与智能体开发",[26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"tdd","quarkus tdd","quarkus","development","camel","services","REST","test","refactoring","use","driven","event-driven","event","for","bugs","features","testing","2026-11-10 10:12:30",false,"---\nname: quarkus-tdd\ndescription: 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.\norigin: ECC\n---\n\n# Quarkus TDD Workflow\n\nTDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.\n\n## When to Use\n\n- New features or REST endpoints\n- Bug fixes or refactors\n- Adding data access logic, security rules, or reactive streams\n- Testing Apache Camel routes and event handlers\n- Testing event-driven services with RabbitMQ\n- Testing conditional flow logic\n- Validating CompletableFuture async operations\n- Testing LogContext propagation\n\n## Workflow\n\n1. Write tests first (they should fail)\n2. Implement minimal code to pass\n3. Refactor with tests green\n4. Enforce coverage with JaCoCo (80%+ target)\n\n## Unit Tests with @Nested Organization\n\nFollow this structured approach for comprehensive, readable tests:\n\n```java\n@ExtendWith(MockitoExtension.class)\n@DisplayName(\"OrderService Unit Tests\")\nclass OrderServiceTest {\n  \n  @Mock\n  private OrderRepository orderRepository;\n  \n  @Mock\n  private EventService eventService;\n  \n  @Mock\n  private FulfillmentPublisher fulfillmentPublisher;\n  \n  @InjectMocks\n  private OrderService orderService;\n  \n  private CreateOrderCommand validCommand;\n\n  @BeforeEach\n  void setUp() {\n    validCommand = new CreateOrderCommand(\n        \"customer-123\",\n        List.of(new OrderLine(\"sku-123\", 2))\n    );\n  }\n\n  @Nested\n  @DisplayName(\"Tests for createOrder\")\n  class CreateOrder {\n    \n    @Test\n    @DisplayName(\"Should persist order and publish fulfillment event\")\n    void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {\n      \u002F\u002F ARRANGE\n      doNothing().when(orderRepository).persist(any(Order.class));\n      \n      \u002F\u002F ACT\n      OrderReceipt receipt = orderService.createOrder(validCommand);\n      \n      \u002F\u002F ASSERT\n      assertThat(receipt).isNotNull();\n      assertThat(receipt.customerId()).isEqualTo(\"customer-123\");\n      verify(orderRepository).persist(any(Order.class));\n      verify(fulfillmentPublisher).publishAsync(receipt);\n      verify(eventService).createSuccessEvent(receipt, \"ORDER_CREATED\");\n    }\n\n    @Test\n    @DisplayName(\"Should reject missing customer id\")\n    void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {\n      \u002F\u002F ARRANGE\n      CreateOrderCommand invalid = new CreateOrderCommand(\"\", validCommand.lines());\n      \n      \u002F\u002F ACT & ASSERT\n      WebApplicationException exception = assertThrows(\n          WebApplicationException.class,\n          () -> orderService.createOrder(invalid)\n      );\n\n      assertThat(exception.getResponse().getStatus()).isEqualTo(400);\n      verify(orderRepository, never()).persist(any(Order.class));\n      verify(fulfillmentPublisher, never()).publishAsync(any());\n    }\n\n    @Test\n    @DisplayName(\"Should record error event when persistence fails\")\n    void givenPersistenceFailure_whenCreateOrder_thenRecordsErrorEvent() {\n      \u002F\u002F ARRANGE\n      doThrow(new PersistenceException(\"database unavailable\"))\n          .when(orderRepository).persist(any(Order.class));\n      \n      \u002F\u002F ACT & ASSERT\n      PersistenceException exception = assertThrows(\n          PersistenceException.class,\n          () -> orderService.createOrder(validCommand)\n      );\n      \n      assertThat(exception.getMessage()).contains(\"database unavailable\");\n      verify(eventService).createErrorEvent(\n          eq(validCommand),\n          eq(\"ORDER_CREATE_FAILED\"),\n          contains(\"database unavailable\")\n      );\n      verify(fulfillmentPublisher, never()).publishAsync(any());\n    }\n\n    @Test\n    @DisplayName(\"Should reject null commands\")\n    void givenNullCommand_whenCreateOrder_thenThrowsNullPointerException() {\n      \u002F\u002F ACT & ASSERT\n      assertThrows(\n          NullPointerException.class,\n          () -> orderService.createOrder(null)\n      );\n      \n      verify(orderRepository, never()).persist(any(Order.class));\n    }\n  }\n}\n```\n\n### Key Testing Patterns\n\n1. **@Nested Classes**: Group tests by method being tested\n2. **@DisplayName**: Provide readable test descriptions for test reports\n3. **Naming Convention**: `givenX_whenY_thenZ` for clarity\n4. **AAA Pattern**: Explicit `\u002F\u002F ARRANGE`, `\u002F\u002F ACT`, `\u002F\u002F ASSERT` comments\n5. **@BeforeEach**: Setup common test data to reduce duplication\n6. **assertDoesNotThrow**: Test success scenarios without catching exceptions\n7. **assertThrows**: Test exception scenarios with message validation using AssertJ\n8. **Comprehensive Coverage**: Test happy paths, null inputs, edge cases, exceptions\n9. **Verify Interactions**: Use Mockito `verify()` to ensure methods are called correctly\n10. **Never Verify**: Use `never()` to ensure methods are NOT called in error scenarios\n\n## Testing Camel Routes\n\n```java\n@QuarkusTest\n@DisplayName(\"Business Rules Camel Route Tests\")\nclass BusinessRulesRouteTest {\n\n  @Inject\n  CamelContext camelContext;\n\n  @Inject\n  ProducerTemplate producerTemplate;\n\n  @InjectMock\n  EventService eventService;\n\n  @InjectMock\n  DocumentValidator documentValidator;\n\n  private BusinessRulesPayload testPayload;\n\n  @BeforeEach\n  void setUp() {\n    \u002F\u002F ARRANGE - Test data\n    testPayload = new BusinessRulesPayload();\n    testPayload.setDocumentId(1L);\n    testPayload.setFlowProfile(FlowProfile.BASIC);\n  }\n\n  @Nested\n  @DisplayName(\"Tests for business-rules-publisher route\")\n  class BusinessRulesPublisher {\n\n    @Test\n    @DisplayName(\"Should successfully publish message to RabbitMQ\")\n    void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {\n      \u002F\u002F ARRANGE\n      MockEndpoint mockRabbitMQ = camelContext.getEndpoint(\"mock:rabbitmq\", MockEndpoint.class);\n      mockRabbitMQ.expectedMessageCount(1);\n      \n      \u002F\u002F Replace real endpoint with mock for testing\n      camelContext.getRouteController().stopRoute(\"business-rules-publisher\");\n      AdviceWith.adviceWith(camelContext, \"business-rules-publisher\", advice -> {\n        advice.replaceFromWith(\"direct:business-rules-publisher\");\n        advice.weaveByToString(\".*spring-rabbitmq.*\").replace().to(\"mock:rabbitmq\");\n      });\n      camelContext.getRouteController().startRoute(\"business-rules-publisher\");\n      \n      \u002F\u002F ACT\n      producerTemplate.sendBody(\"direct:business-rules-publisher\", testPayload);\n      \n      \u002F\u002F ASSERT — body is a JSON String after .marshal().json(JsonLibrary.Jackson)\n      mockRabbitMQ.assertIsSatisfied(5000);\n      \n      assertThat(mockRabbitMQ.getExchanges()).hasSize(1);\n      String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);\n      assertThat(body).contains(\"\\\"documentId\\\":1\");\n    }\n\n    @Test\n    @DisplayName(\"Should handle marshalling to JSON\")\n    void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {\n      \u002F\u002F ARRANGE\n      MockEndpoint mockMarshal = new MockEndpoint(\"mock:marshal\");\n      camelContext.addEndpoint(\"mock:marshal\", mockMarshal);\n      mockMarshal.expectedMessageCount(1);\n      \n      camelContext.getRouteController().stopRoute(\"business-rules-publisher\");\n      AdviceWith.adviceWith(camelContext, \"business-rules-publisher\", advice -> {\n        advice.weaveAddLast().to(\"mock:marshal\");\n      });\n      camelContext.getRouteController().startRoute(\"business-rules-publisher\");\n      \n      \u002F\u002F ACT\n      producerTemplate.sendBody(\"direct:business-rules-publisher\", testPayload);\n      \n      \u002F\u002F ASSERT\n      mockMarshal.assertIsSatisfied(5000);\n      \n      String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);\n      assertThat(body).contains(\"\\\"documentId\\\":1\");\n      assertThat(body).contains(\"\\\"flowProfile\\\":\\\"BASIC\\\"\");\n    }\n  }\n\n  @Nested\n  @DisplayName(\"Tests for document-processing route\")\n  class DocumentProcessing {\n\n    @Test\n    @DisplayName(\"Should route invoice to correct processor\")\n    void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {\n      \u002F\u002F ARRANGE\n      MockEndpoint mockInvoice = camelContext.getEndpoint(\"mock:invoice\", MockEndpoint.class);\n      mockInvoice.expectedMessageCount(1);\n      \n      camelContext.getRouteController().stopRoute(\"document-processing\");\n      AdviceWith.adviceWith(camelContext, \"document-processing\", advice -> {\n        advice.weaveByToString(\".*direct:process-invoice.*\").replace().to(\"mock:invoice\");\n      });\n      camelContext.getRouteController().startRoute(\"document-processing\");\n      \n      \u002F\u002F ACT\n      producerTemplate.sendBodyAndHeader(\"direct:process-document\", \n          testPayload, \"documentType\", \"INVOICE\");\n      \n      \u002F\u002F ASSERT\n      mockInvoice.assertIsSatisfied(5000);\n    }\n\n    @Test\n    @DisplayName(\"Should handle validation errors gracefully\")\n    void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {\n      \u002F\u002F ARRANGE\n      MockEndpoint mockError = camelContext.getEndpoint(\"mock:error\", MockEndpoint.class);\n      mockError.expectedMessageCount(1);\n      \n      camelContext.getRouteController().stopRoute(\"document-processing\");\n      AdviceWith.adviceWith(camelContext, \"document-processing\", advice -> {\n        advice.weaveByToString(\".*direct:validation-error-handler.*\")\n            .replace().to(\"mock:error\");\n      });\n      camelContext.getRouteController().startRoute(\"document-processing\");\n      \n      \u002F\u002F Mock validator bean to throw exception\n      when(documentValidator.validate(any())).thenThrow(new ValidationException(\"Invalid document\"));\n      \n      \u002F\u002F ACT\n      producerTemplate.sendBody(\"direct:process-document\", testPayload);\n      \n      \u002F\u002F ASSERT\n      mockError.assertIsSatisfied(5000);\n      \n      Exception exception = mockError.getExchanges().get(0).getException();\n      assertThat(exception).isInstanceOf(ValidationException.class);\n      assertThat(exception.getMessage()).contains(\"Invalid document\");\n    }\n  }\n}\n```\n\n## Testing Event Services\n\n```java\n@ExtendWith(MockitoExtension.class)\n@DisplayName(\"EventService Unit Tests\")\nclass EventServiceTest {\n\n  @Mock\n  private EventRepository eventRepository;\n  \n  @Mock\n  private ObjectMapper objectMapper;\n  \n  @InjectMocks\n  private EventService eventService;\n  \n  private BusinessRulesPayload testPayload;\n\n  @BeforeEach\n  void setUp() {\n    \u002F\u002F ARRANGE\n    testPayload = new BusinessRulesPayload();\n    testPayload.setDocumentId(1L);\n  }\n\n  @Nested\n  @DisplayName(\"Tests for createSuccessEvent\")\n  class CreateSuccessEvent {\n    \n    @Test\n    @DisplayName(\"Should create success event with correct attributes\")\n    void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {\n      \u002F\u002F ARRANGE\n      when(objectMapper.writeValueAsString(testPayload)).thenReturn(\"{\\\"documentId\\\":1}\");\n      \n      \u002F\u002F ACT\n      assertDoesNotThrow(() -> \n          eventService.createSuccessEvent(testPayload, \"DOCUMENT_PROCESSED\"));\n      \n      \u002F\u002F ASSERT\n      verify(eventRepository).persist(argThat(event -> \n          event.getType().equals(\"DOCUMENT_PROCESSED\") &&\n          event.getStatus() == EventStatus.SUCCESS &&\n          event.getPayload().equals(\"{\\\"documentId\\\":1}\") &&\n          event.getTimestamp() != null\n      ));\n    }\n\n    @Test\n    @DisplayName(\"Should throw exception when payload is null\")\n    void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {\n      \u002F\u002F ARRANGE\n      Object nullPayload = null;\n      \n      \u002F\u002F ACT & ASSERT\n      NullPointerException exception = assertThrows(\n          NullPointerException.class,\n          () -> eventService.createSuccessEvent(nullPayload, \"EVENT_TYPE\")\n      );\n      \n      assertThat(exception.getMessage()).isEqualTo(\"Payload cannot be null\");\n      verify(eventRepository, never()).persist(any());\n    }\n  }\n\n  @Nested\n  @DisplayName(\"Tests for createErrorEvent\")\n  class CreateErrorEvent {\n    \n    @Test\n    @DisplayName(\"Should create error event with error message\")\n    void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {\n      \u002F\u002F ARRANGE\n      String errorMessage = \"Processing failed\";\n      when(objectMapper.writeValueAsString(testPayload)).thenReturn(\"{\\\"documentId\\\":1}\");\n      \n      \u002F\u002F ACT\n      assertDoesNotThrow(() -> \n          eventService.createErrorEvent(testPayload, \"PROCESSING_ERROR\", errorMessage));\n      \n      \u002F\u002F ASSERT\n      verify(eventRepository).persist(argThat(event -> \n          event.getType().equals(\"PROCESSING_ERROR\") &&\n          event.getStatus() == EventStatus.ERROR &&\n          event.getErrorMessage().equals(errorMessage) &&\n          event.getPayload().equals(\"{\\\"documentId\\\":1}\")\n      ));\n    }\n\n    @ParameterizedTest\n    @DisplayName(\"Should reject invalid error messages\")\n    @ValueSource(strings = {\"\", \" \"})\n    void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {\n      \u002F\u002F ACT & ASSERT\n      IllegalArgumentException exception = assertThrows(\n          IllegalArgumentException.class,\n          () -> eventService.createErrorEvent(testPayload, \"ERROR\", blankMessage)\n      );\n      \n      assertThat(exception.getMessage()).contains(\"Error message cannot be blank\");\n    }\n  }\n}\n```\n\n## Testing CompletableFuture\n\n```java\n@ExtendWith(MockitoExtension.class)\n@DisplayName(\"FileStorageService Unit Tests\")\nclass FileStorageServiceTest {\n\n  @Mock\n  private S3Client s3Client;\n  \n  @Mock\n  private ExecutorService executorService;\n  \n  @InjectMocks\n  private FileStorageService fileStorageService;\n  \n  private InputStream testInputStream;\n  private LogContext testLogContext;\n\n  @BeforeEach\n  void setUp() {\n    \u002F\u002F ARRANGE\n    testInputStream = new ByteArrayInputStream(\"test content\".getBytes());\n    testLogContext = new LogContext();\n    testLogContext.put(\"traceId\", \"trace-123\");\n  }\n\n  @Nested\n  @DisplayName(\"Tests for uploadOriginalFile\")\n  class UploadOriginalFile {\n    \n    @Test\n    @DisplayName(\"Should successfully upload file and return document info\")\n    void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {\n      \u002F\u002F ARRANGE\n      doAnswer(invocation -> {\n        ((Runnable) invocation.getArgument(0)).run();\n        return null;\n      }).when(executorService).execute(any(Runnable.class));\n      \n      when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))\n          .thenReturn(PutObjectResponse.builder().build());\n      \n      \u002F\u002F ACT\n      CompletableFuture\u003CStoredDocumentInfo> future = \n          fileStorageService.uploadOriginalFile(testInputStream, 1024L, \n              testLogContext, InvoiceFormat.UBL);\n      \n      StoredDocumentInfo result = future.join();\n      \n      \u002F\u002F ASSERT\n      assertThat(result).isNotNull();\n      assertThat(result.getPath()).isNotBlank();\n      assertThat(result.getSize()).isEqualTo(1024L);\n      assertThat(result.getUploadedAt()).isNotNull();\n      \n      verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));\n    }\n\n    @Test\n    @DisplayName(\"Should handle S3 upload failure\")\n    void givenS3Failure_whenUpload_thenCompletableFutureFails() {\n      \u002F\u002F ARRANGE — run synchronously so exception propagates through the future\n      doAnswer(invocation -> {\n        ((Runnable) invocation.getArgument(0)).run();\n        return null;\n      }).when(executorService).execute(any(Runnable.class));\n      \n      when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))\n          .thenThrow(new StorageException(\"S3 unavailable\"));\n      \n      \u002F\u002F ACT\n      CompletableFuture\u003CStoredDocumentInfo> future = \n          fileStorageService.uploadOriginalFile(testInputStream, 1024L, \n              testLogContext, InvoiceFormat.UBL);\n      \n      \u002F\u002F ASSERT\n      assertThatThrownBy(() -> future.join())\n          .isInstanceOf(CompletionException.class)\n          .hasCauseInstanceOf(StorageException.class)\n          .hasMessageContaining(\"S3 unavailable\");\n    }\n\n    @Test\n    @DisplayName(\"Should propagate LogContext to async operation\")\n    void givenLogContext_whenUpload_thenContextPropagated() throws Exception {\n      \u002F\u002F ARRANGE\n      AtomicReference\u003CLogContext> capturedContext = new AtomicReference\u003C>();\n      \n      doAnswer(invocation -> {\n        capturedContext.set(CustomLog.getCurrentContext());\n        ((Runnable) invocation.getArgument(0)).run();\n        return null;\n      }).when(executorService).execute(any(Runnable.class));\n      \n      \u002F\u002F ACT\n      fileStorageService.uploadOriginalFile(testInputStream, 1024L, \n          testLogContext, InvoiceFormat.UBL).join();\n      \n      \u002F\u002F ASSERT\n      assertThat(capturedContext.get()).isNotNull();\n      assertThat(capturedContext.get().get(\"traceId\")).isEqualTo(\"trace-123\");\n    }\n  }\n}\n```\n\n## Resource Layer Tests (REST Assured)\n\n```java\n@QuarkusTest\n@DisplayName(\"DocumentResource API Tests\")\nclass DocumentResourceTest {\n\n  @InjectMock\n  DocumentService documentService;\n\n  @Nested\n  @DisplayName(\"Tests for GET \u002Fapi\u002Fdocuments\")\n  class ListDocuments {\n\n    @Test\n    @DisplayName(\"Should return list of documents\")\n    void givenDocumentsExist_whenList_thenReturnsOk() {\n      \u002F\u002F ARRANGE\n      List\u003CDocument> documents = List.of(createDocument(1L, \"DOC-001\"));\n      when(documentService.list(0, 20)).thenReturn(documents);\n\n      \u002F\u002F ACT & ASSERT\n      given()\n          .when().get(\"\u002Fapi\u002Fdocuments\")\n          .then()\n          .statusCode(200)\n          .body(\"$.size()\", is(1))\n          .body(\"[0].referenceNumber\", equalTo(\"DOC-001\"));\n    }\n  }\n\n  @Nested\n  @DisplayName(\"Tests for POST \u002Fapi\u002Fdocuments\")\n  class CreateDocument {\n\n    @Test\n    @DisplayName(\"Should create document and return 201\")\n    void givenValidRequest_whenCreate_thenReturns201() {\n      \u002F\u002F ARRANGE\n      Document document = createDocument(1L, \"DOC-001\");\n      when(documentService.create(any())).thenReturn(document);\n\n      \u002F\u002F ACT & ASSERT\n      given()\n          .contentType(ContentType.JSON)\n          .body(\"\"\"\n              {\n                \"referenceNumber\": \"DOC-001\",\n                \"description\": \"Test document\",\n                \"validUntil\": \"2030-01-01T00:00:00Z\",\n                \"categories\": [\"test\"]\n              }\n              \"\"\")\n          .when().post(\"\u002Fapi\u002Fdocuments\")\n          .then()\n          .statusCode(201)\n          .header(\"Location\", containsString(\"\u002Fapi\u002Fdocuments\u002F1\"))\n          .body(\"referenceNumber\", equalTo(\"DOC-001\"));\n    }\n\n    @Test\n    @DisplayName(\"Should return 400 for invalid input\")\n    void givenInvalidRequest_whenCreate_thenReturns400() {\n      \u002F\u002F ACT & ASSERT\n      given()\n          .contentType(ContentType.JSON)\n          .body(\"\"\"\n              {\n                \"referenceNumber\": \"\",\n                \"description\": \"Test\"\n              }\n              \"\"\")\n          .when().post(\"\u002Fapi\u002Fdocuments\")\n          .then()\n          .statusCode(400);\n    }\n  }\n\n  private Document createDocument(Long id, String referenceNumber) {\n    Document document = new Document();\n    document.setId(id);\n    document.setReferenceNumber(referenceNumber);\n    document.setStatus(DocumentStatus.PENDING);\n    return document;\n  }\n}\n```\n\n## Integration Tests with Real Database\n\n```java\n@QuarkusTest\n@TestProfile(IntegrationTestProfile.class)\n@DisplayName(\"Document Integration Tests\")\nclass DocumentIntegrationTest {\n\n  @Test\n  @Transactional\n  @DisplayName(\"Should create and retrieve document via API\")\n  void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {\n    \u002F\u002F ACT - Create via API\n    Long id = given()\n        .contentType(ContentType.JSON)\n        .body(\"\"\"\n            {\n              \"referenceNumber\": \"INT-001\",\n              \"description\": \"Integration test\",\n              \"validUntil\": \"2030-01-01T00:00:00Z\",\n              \"categories\": [\"test\"]\n            }\n            \"\"\")\n        .when().post(\"\u002Fapi\u002Fdocuments\")\n        .then()\n        .statusCode(201)\n        .extract().path(\"id\");\n\n    \u002F\u002F ASSERT - Retrieve via API\n    given()\n        .when().get(\"\u002Fapi\u002Fdocuments\u002F\" + id)\n        .then()\n        .statusCode(200)\n        .body(\"referenceNumber\", equalTo(\"INT-001\"));\n  }\n}\n```\n\n## Coverage with JaCoCo\n\n### Maven Configuration (Complete)\n\n```xml\n\u003Cplugin>\n  \u003CgroupId>org.jacoco\u003C\u002FgroupId>\n  \u003CartifactId>jacoco-maven-plugin\u003C\u002FartifactId>\n  \u003Cversion>0.8.13\u003C\u002Fversion>\n  \u003Cexecutions>\n    \u003C!-- Prepare agent for test execution -->\n    \u003Cexecution>\n      \u003Cid>prepare-agent\u003C\u002Fid>\n      \u003Cgoals>\n        \u003Cgoal>prepare-agent\u003C\u002Fgoal>\n      \u003C\u002Fgoals>\n    \u003C\u002Fexecution>\n    \n    \u003C!-- Generate coverage report -->\n    \u003Cexecution>\n      \u003Cid>report\u003C\u002Fid>\n      \u003Cphase>verify\u003C\u002Fphase>\n      \u003Cgoals>\n        \u003Cgoal>report\u003C\u002Fgoal>\n      \u003C\u002Fgoals>\n    \u003C\u002Fexecution>\n    \n    \u003C!-- Enforce coverage thresholds -->\n    \u003Cexecution>\n      \u003Cid>check\u003C\u002Fid>\n      \u003Cgoals>\n        \u003Cgoal>check\u003C\u002Fgoal>\n      \u003C\u002Fgoals>\n      \u003Cconfiguration>\n        \u003Crules>\n          \u003Crule>\n            \u003Celement>BUNDLE\u003C\u002Felement>\n            \u003Climits>\n              \u003Climit>\n                \u003Ccounter>LINE\u003C\u002Fcounter>\n                \u003Cvalue>COVEREDRATIO\u003C\u002Fvalue>\n                \u003Cminimum>0.80\u003C\u002Fminimum>\n              \u003C\u002Flimit>\n              \u003Climit>\n                \u003Ccounter>BRANCH\u003C\u002Fcounter>\n                \u003Cvalue>COVEREDRATIO\u003C\u002Fvalue>\n                \u003Cminimum>0.70\u003C\u002Fminimum>\n              \u003C\u002Flimit>\n            \u003C\u002Flimits>\n          \u003C\u002Frule>\n        \u003C\u002Frules>\n      \u003C\u002Fconfiguration>\n    \u003C\u002Fexecution>\n  \u003C\u002Fexecutions>\n\u003C\u002Fplugin>\n```\n\nRun tests with coverage:\n```bash\nmvn clean test\nmvn jacoco:report\nmvn jacoco:check\n\n# Report at: target\u002Fsite\u002Fjacoco\u002Findex.html\n```\n\n## Test Dependencies\n\n```xml\n\u003Cdependencies>\n    \u003C!-- Quarkus Testing -->\n    \u003Cdependency>\n        \u003CgroupId>io.quarkus\u003C\u002FgroupId>\n        \u003CartifactId>quarkus-junit5\u003C\u002FartifactId>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n    \u003Cdependency>\n        \u003CgroupId>io.quarkus\u003C\u002FgroupId>\n        \u003CartifactId>quarkus-junit5-mockito\u003C\u002FartifactId>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n    \n    \u003C!-- Mockito -->\n    \u003Cdependency>\n        \u003CgroupId>org.mockito\u003C\u002FgroupId>\n        \u003CartifactId>mockito-core\u003C\u002FartifactId>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n    \n    \u003C!-- AssertJ (preferred over JUnit assertions) -->\n    \u003Cdependency>\n        \u003CgroupId>org.assertj\u003C\u002FgroupId>\n        \u003CartifactId>assertj-core\u003C\u002FartifactId>\n        \u003Cversion>3.24.2\u003C\u002Fversion>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n    \n    \u003C!-- REST Assured -->\n    \u003Cdependency>\n        \u003CgroupId>io.rest-assured\u003C\u002FgroupId>\n        \u003CartifactId>rest-assured\u003C\u002FartifactId>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n    \n    \u003C!-- Camel Testing -->\n    \u003Cdependency>\n        \u003CgroupId>org.apache.camel.quarkus\u003C\u002FgroupId>\n        \u003CartifactId>camel-quarkus-junit5\u003C\u002FartifactId>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n\u003C\u002Fdependencies>\n```\n\n## Best Practices\n\n### Test Organization\n- Use `@Nested` classes to group tests by method being tested\n- Use `@DisplayName` for readable test descriptions visible in reports\n- Follow `givenX_whenY_thenZ` naming convention for test methods\n- Use `@BeforeEach` for common test data setup to reduce duplication\n\n### Test Structure\n- Follow AAA pattern with explicit comments (`\u002F\u002F ARRANGE`, `\u002F\u002F ACT`, `\u002F\u002F ASSERT`)\n- Use `assertDoesNotThrow` for success scenarios\n- Use `assertThrows` for exception scenarios with message validation\n- Verify exception messages match expected values using AssertJ `contains()` or `isEqualTo()`\n\n### Test Coverage\n- Test happy paths for all public methods\n- Test null input handling\n- Test edge cases (empty collections, boundary values, negative IDs, blank strings)\n- Test exception scenarios comprehensively\n- Mock all external dependencies (repositories, services, Camel endpoints)\n- Aim for 80%+ line coverage, 70%+ branch coverage\n\n### Assertions\n- **Prefer AssertJ** (`assertThat`) over JUnit assertions for value checks\n- Use fluent AssertJ API for readability: `assertThat(list).hasSize(3).contains(item)`\n- For exceptions: use JUnit `assertThrows` to capture, then AssertJ to validate the message\n- For non-throwing success paths: use JUnit `assertDoesNotThrow`\n- For collections: `extracting()`, `filteredOn()`, `containsExactly()`\n\n### Testing Integration\n- Use `@QuarkusTest` for integration tests\n- Use `@InjectMock` to mock dependencies in Quarkus tests\n- Prefer REST Assured for API testing\n- Use `@TestProfile` for test-specific configuration\n\n### Event-Driven Testing\n- Test Camel routes with `AdviceWith` and `MockEndpoint`\n- Use `@CamelQuarkusTest` annotation (if using standalone Camel tests)\n- Verify message content, headers, and routing logic\n- Test error handling routes separately\n- Mock external systems (RabbitMQ, S3, databases) in unit tests\n\n### Camel Route Testing\n- Use `MockEndpoint` for asserting message flow\n- Use `AdviceWith` to modify routes for testing (replace endpoints with mocks)\n- Test message transformation and marshalling\n- Test exception handling and dead letter queues\n\n### Testing Async Operations\n- Test CompletableFuture success and failure scenarios\n- Use `.join()` in tests to wait for async completion\n- Test exception propagation from CompletableFuture\n- Verify LogContext propagation to async operations\n\n### Performance\n- Keep tests fast and isolated\n- Run tests in continuous mode: `mvn quarkus:test`\n- Use parameterized tests (`@ParameterizedTest`) for input variations\n- Build reusable test data builders or factory methods\n\n### Quarkus-Specific\n- Stay on latest LTS version (Quarkus 3.x)\n- Test native compilation compatibility periodically\n- Use Quarkus test profiles for different scenarios\n- Leverage Quarkus dev services for local testing\n- Use `@InjectMock` instead of `@MockBean` (Quarkus-specific)\n\n### Verification Best Practices\n- Always verify interactions on mocked dependencies\n- Use `verify(mock, never())` to ensure methods are NOT called in error scenarios\n- Use `argThat()` for complex argument matching\n- Verify the order of calls when it matters: `InOrder` from Mockito\n","# Quarkus TDD Workflow\n\nTDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.\n\n## When to Use\n\n- New features or REST endpoints\n- Bug fixes or refactors\n- Adding data access logic, security rules, or reactive streams\n- Testing Apache Camel routes and event handlers\n- Testing event-driven services with RabbitMQ\n- Testing conditional flow logic\n- Validating CompletableFuture async operations\n- Testing LogContext propagation\n\n## Workflow\n\n1. Write tests first (they should fail)\n2. Implement minimal code to pass\n3. Refactor with tests green\n4. Enforce coverage with JaCoCo (80%+ target)\n\n## Unit Tests with @Nested Organization\n\nFollow this structured approach for comprehensive, readable tests:\n\n```java\n@ExtendWith(MockitoExtension.class)\n@DisplayName(\"OrderService Unit Tests\")\nclass OrderServiceTest {\n  \n  @Mock\n  private OrderRepository orderRepository;\n  \n  @Mock\n  private EventService eventService;\n  \n  @Mock\n  private FulfillmentPublisher fulfillmentPublisher;\n  \n  @InjectMocks\n  private OrderService orderService;\n  \n  private CreateOrderCommand validCommand;\n\n  @BeforeEach\n  void setUp() {\n    validCommand = new CreateOrderCommand(\n        \"customer-123\",\n        List.of(new OrderLine(\"sku-123\", 2))\n    );\n  }\n\n  @Nested\n  @DisplayName(\"Tests for createOrder\")\n  class CreateOrder {\n    \n    @Test\n    @DisplayName(\"Should persist order and publish fulfillment event\")\n    void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {\n      \u002F\u002F ARRANGE\n      doNothing().when(orderRepository).persist(any(Order.class));\n      \n      \u002F\u002F ACT\n      OrderReceipt receipt = orderService.createOrder(validCommand);\n      \n      \u002F\u002F ASSERT\n      assertThat(receipt).isNotNull();\n      assertThat(receipt.customerId()).isEqualTo(\"customer-123\");\n      verify(orderRepository).persist(any(Order.class));\n      verify(fulfillmentPublisher).publishAsync(receipt);\n      verify(eventService).createSuccessEvent(receipt, \"ORDER_CREATED\");\n    }\n\n    @Test\n    @DisplayName(\"Should reject missing customer id\")\n    void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {\n      \u002F\u002F ARRANGE\n      CreateOrderCommand invalid = new CreateOrderCommand(\"\", validCommand.lines());\n      \n      \u002F\u002F ACT & ASSERT\n      WebApplicationException exception = assertThrows(\n          WebApplicationException.class,\n          () -> orderService.createOrder(invalid)\n      );\n\n      assertThat(exception.getResponse().getStatus()).isEqualTo(400);\n      verify(orderRepository, never()).persist(any(Order.class));\n      verify(fulfillmentPublisher, never()).publishAsync(any());\n    }\n\n    @Test\n    @DisplayName(\"Should record error event when persistence fails\")\n    void givenPersistenceFailure_whenCreateOrder_thenRecordsErrorEvent() {\n      \u002F\u002F ARRANGE\n      doThrow(new PersistenceException(\"database unavailable\"))\n          .when(orderRepository).persist(any(Order.class));\n      \n      \u002F\u002F ACT & ASSERT\n      PersistenceException exception = assertThrows(\n          PersistenceException.class,\n          () -> orderService.createOrder(validCommand)\n      );\n      \n      assertThat(exception.getMessage()).contains(\"database unavailable\");\n      verify(eventService).createErrorEvent(\n          eq(validCommand),\n          eq(\"ORDER_CREATE_FAILED\"),\n          contains(\"database unavailable\")\n      );\n      verify(fulfillmentPublisher, never()).publishAsync(any());\n    }\n\n    @Test\n    @DisplayName(\"Should reject null commands\")\n    void givenNullCommand_whenCreateOrder_thenThrowsNullPointerException() {\n      \u002F\u002F ACT & ASSERT\n      assertThrows(\n          NullPointerException.class,\n          () -> orderService.createOrder(null)\n      );\n      \n      verify(orderRepository, never()).persist(any(Order.class));\n    }\n  }\n}\n```\n\n### Key Testing Patterns\n\n1. **@Nested Classes**: Group tests by method being tested\n2. **@DisplayName**: Provide readable test descriptions for test reports\n3. **Naming Convention**: `givenX_whenY_thenZ` for clarity\n4. **AAA Pattern**: Explicit `\u002F\u002F ARRANGE`, `\u002F\u002F ACT`, `\u002F\u002F ASSERT` comments\n5. **@BeforeEach**: Setup common test data to reduce duplication\n6. **assertDoesNotThrow**: Test success scenarios without catching exceptions\n7. **assertThrows**: Test exception scenarios with message validation using AssertJ\n8. **Comprehensive Coverage**: Test happy paths, null inputs, edge cases, exceptions\n9. **Verify Interactions**: Use Mockito `verify()` to ensure methods are called correctly\n10. **Never Verify**: Use `never()` to ensure methods are NOT called in error scenarios\n\n## Testing Camel Routes\n\n```java\n@QuarkusTest\n@DisplayName(\"Business Rules Camel Route Tests\")\nclass BusinessRulesRouteTest {\n\n  @Inject\n  CamelContext camelContext;\n\n  @Inject\n  ProducerTemplate producerTemplate;\n\n  @InjectMock\n  EventService eventService;\n\n  @InjectMock\n  DocumentValidator documentValidator;\n\n  private BusinessRulesPayload testPayload;\n\n  @BeforeEach\n  void setUp() {\n    \u002F\u002F ARRANGE - Test data\n    testPayload = new BusinessRulesPayload();\n    testPayload.setDocumentId(1L);\n    testPayload.setFlowProfile(FlowProfile.BASIC);\n  }\n\n  @Nested\n  @DisplayName(\"Tests for business-rules-publisher route\")\n  class BusinessRulesPublisher {\n\n    @Test\n    @DisplayName(\"Should successfully publish message to RabbitMQ\")\n    void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {\n      \u002F\u002F ARRANGE\n      MockEndpoint mockRabbitMQ = camelContext.getEndpoint(\"mock:rabbitmq\", MockEndpoint.class);\n      mockRabbitMQ.expectedMessageCount(1);\n      \n      \u002F\u002F Replace real endpoint with mock for testing\n      camelContext.getRouteController().stopRoute(\"business-rules-publisher\");\n      AdviceWith.adviceWith(camelContext, \"business-rules-publisher\", advice -> {\n        advice.replaceFromWith(\"direct:business-rules-publisher\");\n        advice.weaveByToString(\".*spring-rabbitmq.*\").replace().to(\"mock:rabbitmq\");\n      });\n      camelContext.getRouteController().startRoute(\"business-rules-publisher\");\n      \n      \u002F\u002F ACT\n      producerTemplate.sendBody(\"direct:business-rules-publisher\", testPayload);\n      \n      \u002F\u002F ASSERT — body is a JSON String after .marshal().json(JsonLibrary.Jackson)\n      mockRabbitMQ.assertIsSatisfied(5000);\n      \n      assertThat(mockRabbitMQ.getExchanges()).hasSize(1);\n      String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);\n      assertThat(body).contains(\"\\\"documentId\\\":1\");\n    }\n\n    @Test\n    @DisplayName(\"Should handle marshalling to JSON\")\n    void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {\n      \u002F\u002F ARRANGE\n      MockEndpoint mockMarshal = new MockEndpoint(\"mock:marshal\");\n      camelContext.addEndpoint(\"mock:marshal\", mockMarshal);\n      mockMarshal.expectedMessageCount(1);\n      \n      camelContext.getRouteController().stopRoute(\"business-rules-publisher\");\n      AdviceWith.adviceWith(camelContext, \"business-rules-publisher\", advice -> {\n        advice.weaveAddLast().to(\"mock:marshal\");\n      });\n      camelContext.getRouteController().startRoute(\"business-rules-publisher\");\n      \n      \u002F\u002F ACT\n      producerTemplate.sendBody(\"direct:business-rules-publisher\", testPayload);\n      \n      \u002F\u002F ASSERT\n      mockMarshal.assertIsSatisfied(5000);\n      \n      String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);\n      assertThat(body).contains(\"\\\"documentId\\\":1\");\n      assertThat(body).contains(\"\\\"flowProfile\\\":\\\"BASIC\\\"\");\n    }\n  }\n\n  @Nested\n  @DisplayName(\"Tests for document-processing route\")\n  class DocumentProcessing {\n\n    @Test\n    @DisplayName(\"Should route invoice to correct processor\")\n    void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {\n      \u002F\u002F ARRANGE\n      MockEndpoint mockInvoice = camelContext.getEndpoint(\"mock:invoice\", MockEndpoint.class);\n      mockInvoice.expectedMessageCount(1);\n      \n      camelContext.getRouteController().stopRoute(\"document-processing\");\n      AdviceWith.adviceWith(camelContext, \"document-processing\", advice -> {\n        advice.weaveByToString(\".*direct:process-invoice.*\").replace().to(\"mock:invoice\");\n      });\n      camelContext.getRouteController().startRoute(\"document-processing\");\n      \n      \u002F\u002F ACT\n      producerTemplate.sendBodyAndHeader(\"direct:process-document\", \n          testPayload, \"documentType\", \"INVOICE\");\n      \n      \u002F\u002F ASSERT\n      mockInvoice.assertIsSatisfied(5000);\n    }\n\n    @Test\n    @DisplayName(\"Should handle validation errors gracefully\")\n    void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {\n      \u002F\u002F ARRANGE\n      MockEndpoint mockError = camelContext.getEndpoint(\"mock:error\", MockEndpoint.class);\n      mockError.expectedMessageCount(1);\n      \n      camelContext.getRouteController().stopRoute(\"document-processing\");\n      AdviceWith.adviceWith(camelContext, \"document-processing\", advice -> {\n        advice.weaveByToString(\".*direct:validation-error-handler.*\")\n            .replace().to(\"mock:error\");\n      });\n      camelContext.getRouteController().startRoute(\"document-processing\");\n      \n      \u002F\u002F Mock validator bean to throw exception\n      when(documentValidator.validate(any())).thenThrow(new ValidationException(\"Invalid document\"));\n      \n      \u002F\u002F ACT\n      producerTemplate.sendBody(\"direct:process-document\", testPayload);\n      \n      \u002F\u002F ASSERT\n      mockError.assertIsSatisfied(5000);\n      \n      Exception exception = mockError.getExchanges().get(0).getException();\n      assertThat(exception).isInstanceOf(ValidationException.class);\n      assertThat(exception.getMessage()).contains(\"Invalid document\");\n    }\n  }\n}\n```\n\n## Testing Event Services\n\n```java\n@ExtendWith(MockitoExtension.class)\n@DisplayName(\"EventService Unit Tests\")\nclass EventServiceTest {\n\n  @Mock\n  private EventRepository eventRepository;\n  \n  @Mock\n  private ObjectMapper objectMapper;\n  \n  @InjectMocks\n  private EventService eventService;\n  \n  private BusinessRulesPayload testPayload;\n\n  @BeforeEach\n  void setUp() {\n    \u002F\u002F ARRANGE\n    testPayload = new BusinessRulesPayload();\n    testPayload.setDocumentId(1L);\n  }\n\n  @Nested\n  @DisplayName(\"Tests for createSuccessEvent\")\n  class CreateSuccessEvent {\n    \n    @Test\n    @DisplayName(\"Should create success event with correct attributes\")\n    void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {\n      \u002F\u002F ARRANGE\n      when(objectMapper.writeValueAsString(testPayload)).thenReturn(\"{\\\"documentId\\\":1}\");\n      \n      \u002F\u002F ACT\n      assertDoesNotThrow(() -> \n          eventService.createSuccessEvent(testPayload, \"DOCUMENT_PROCESSED\"));\n      \n      \u002F\u002F ASSERT\n      verify(eventRepository).persist(argThat(event -> \n          event.getType().equals(\"DOCUMENT_PROCESSED\") &&\n          event.getStatus() == EventStatus.SUCCESS &&\n          event.getPayload().equals(\"{\\\"documentId\\\":1}\") &&\n          event.getTimestamp() != null\n      ));\n    }\n\n    @Test\n    @DisplayName(\"Should throw exception when payload is null\")\n    void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {\n      \u002F\u002F ARRANGE\n      Object nullPayload = null;\n      \n      \u002F\u002F ACT & ASSERT\n      NullPointerException exception = assertThrows(\n          NullPointerException.class,\n          () -> eventService.createSuccessEvent(nullPayload, \"EVENT_TYPE\")\n      );\n      \n      assertThat(exception.getMessage()).isEqualTo(\"Payload cannot be null\");\n      verify(eventRepository, never()).persist(any());\n    }\n  }\n\n  @Nested\n  @DisplayName(\"Tests for createErrorEvent\")\n  class CreateErrorEvent {\n    \n    @Test\n    @DisplayName(\"Should create error event with error message\")\n    void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {\n      \u002F\u002F ARRANGE\n      String errorMessage = \"Processing failed\";\n      when(objectMapper.writeValueAsString(testPayload)).thenReturn(\"{\\\"documentId\\\":1}\");\n      \n      \u002F\u002F ACT\n      assertDoesNotThrow(() -> \n          eventService.createErrorEvent(testPayload, \"PROCESSING_ERROR\", errorMessage));\n      \n      \u002F\u002F ASSERT\n      verify(eventRepository).persist(argThat(event -> \n          event.getType().equals(\"PROCESSING_ERROR\") &&\n          event.getStatus() == EventStatus.ERROR &&\n          event.getErrorMessage().equals(errorMessage) &&\n          event.getPayload().equals(\"{\\\"documentId\\\":1}\")\n      ));\n    }\n\n    @ParameterizedTest\n    @DisplayName(\"Should reject invalid error messages\")\n    @ValueSource(strings = {\"\", \" \"})\n    void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {\n      \u002F\u002F ACT & ASSERT\n      IllegalArgumentException exception = assertThrows(\n          IllegalArgumentException.class,\n          () -> eventService.createErrorEvent(testPayload, \"ERROR\", blankMessage)\n      );\n      \n      assertThat(exception.getMessage()).contains(\"Error message cannot be blank\");\n    }\n  }\n}\n```\n\n## Testing CompletableFuture\n\n```java\n@ExtendWith(MockitoExtension.class)\n@DisplayName(\"FileStorageService Unit Tests\")\nclass FileStorageServiceTest {\n\n  @Mock\n  private S3Client s3Client;\n  \n  @Mock\n  private ExecutorService executorService;\n  \n  @InjectMocks\n  private FileStorageService fileStorageService;\n  \n  private InputStream testInputStream;\n  private LogContext testLogContext;\n\n  @BeforeEach\n  void setUp() {\n    \u002F\u002F ARRANGE\n    testInputStream = new ByteArrayInputStream(\"test content\".getBytes());\n    testLogContext = new LogContext();\n    testLogContext.put(\"traceId\", \"trace-123\");\n  }\n\n  @Nested\n  @DisplayName(\"Tests for uploadOriginalFile\")\n  class UploadOriginalFile {\n    \n    @Test\n    @DisplayName(\"Should successfully upload file and return document info\")\n    void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {\n      \u002F\u002F ARRANGE\n      doAnswer(invocation -> {\n        ((Runnable) invocation.getArgument(0)).run();\n        return null;\n      }).when(executorService).execute(any(Runnable.class));\n      \n      when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))\n          .thenReturn(PutObjectResponse.builder().build());\n      \n      \u002F\u002F ACT\n      CompletableFuture\u003CStoredDocumentInfo> future = \n          fileStorageService.uploadOriginalFile(testInputStream, 1024L, \n              testLogContext, InvoiceFormat.UBL);\n      \n      StoredDocumentInfo result = future.join();\n      \n      \u002F\u002F ASSERT\n      assertThat(result).isNotNull();\n      assertThat(result.getPath()).isNotBlank();\n      assertThat(result.getSize()).isEqualTo(1024L);\n      assertThat(result.getUploadedAt()).isNotNull();\n      \n      verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));\n    }\n\n    @Test\n    @DisplayName(\"Should handle S3 upload failure\")\n    void givenS3Failure_whenUpload_thenCompletableFutureFails() {\n      \u002F\u002F ARRANGE — run synchronously so exception propagates through the future\n      doAnswer(invocation -> {\n        ((Runnable) invocation.getArgument(0)).run();\n        return null;\n      }).when(executorService).execute(any(Runnable.class));\n      \n      when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))\n          .thenThrow(new StorageException(\"S3 unavailable\"));\n      \n      \u002F\u002F ACT\n      CompletableFuture\u003CStoredDocumentInfo> future = \n          fileStorageService.uploadOriginalFile(testInputStream, 1024L, \n              testLogContext, InvoiceFormat.UBL);\n      \n      \u002F\u002F ASSERT\n      assertThatThrownBy(() -> future.join())\n          .isInstanceOf(CompletionException.class)\n          .hasCauseInstanceOf(StorageException.class)\n          .hasMessageContaining(\"S3 unavailable\");\n    }\n\n    @Test\n    @DisplayName(\"Should propagate LogContext to async operation\")\n    void givenLogContext_whenUpload_thenContextPropagated() throws Exception {\n      \u002F\u002F ARRANGE\n      AtomicReference\u003CLogContext> capturedContext = new AtomicReference\u003C>();\n      \n      doAnswer(invocation -> {\n        capturedContext.set(CustomLog.getCurrentContext());\n        ((Runnable) invocation.getArgument(0)).run();\n        return null;\n      }).when(executorService).execute(any(Runnable.class));\n      \n      \u002F\u002F ACT\n      fileStorageService.uploadOriginalFile(testInputStream, 1024L, \n          testLogContext, InvoiceFormat.UBL).join();\n      \n      \u002F\u002F ASSERT\n      assertThat(capturedContext.get()).isNotNull();\n      assertThat(capturedContext.get().get(\"traceId\")).isEqualTo(\"trace-123\");\n    }\n  }\n}\n```\n\n## Resource Layer Tests (REST Assured)\n\n```java\n@QuarkusTest\n@DisplayName(\"DocumentResource API Tests\")\nclass DocumentResourceTest {\n\n  @InjectMock\n  DocumentService documentService;\n\n  @Nested\n  @DisplayName(\"Tests for GET \u002Fapi\u002Fdocuments\")\n  class ListDocuments {\n\n    @Test\n    @DisplayName(\"Should return list of documents\")\n    void givenDocumentsExist_whenList_thenReturnsOk() {\n      \u002F\u002F ARRANGE\n      List\u003CDocument> documents = List.of(createDocument(1L, \"DOC-001\"));\n      when(documentService.list(0, 20)).thenReturn(documents);\n\n      \u002F\u002F ACT & ASSERT\n      given()\n          .when().get(\"\u002Fapi\u002Fdocuments\")\n          .then()\n          .statusCode(200)\n          .body(\"$.size()\", is(1))\n          .body(\"[0].referenceNumber\", equalTo(\"DOC-001\"));\n    }\n  }\n\n  @Nested\n  @DisplayName(\"Tests for POST \u002Fapi\u002Fdocuments\")\n  class CreateDocument {\n\n    @Test\n    @DisplayName(\"Should create document and return 201\")\n    void givenValidRequest_whenCreate_thenReturns201() {\n      \u002F\u002F ARRANGE\n      Document document = createDocument(1L, \"DOC-001\");\n      when(documentService.create(any())).thenReturn(document);\n\n      \u002F\u002F ACT & ASSERT\n      given()\n          .contentType(ContentType.JSON)\n          .body(\"\"\"\n              {\n                \"referenceNumber\": \"DOC-001\",\n                \"description\": \"Test document\",\n                \"validUntil\": \"2030-01-01T00:00:00Z\",\n                \"categories\": [\"test\"]\n              }\n              \"\"\")\n          .when().post(\"\u002Fapi\u002Fdocuments\")\n          .then()\n          .statusCode(201)\n          .header(\"Location\", containsString(\"\u002Fapi\u002Fdocuments\u002F1\"))\n          .body(\"referenceNumber\", equalTo(\"DOC-001\"));\n    }\n\n    @Test\n    @DisplayName(\"Should return 400 for invalid input\")\n    void givenInvalidRequest_whenCreate_thenReturns400() {\n      \u002F\u002F ACT & ASSERT\n      given()\n          .contentType(ContentType.JSON)\n          .body(\"\"\"\n              {\n                \"referenceNumber\": \"\",\n                \"description\": \"Test\"\n              }\n              \"\"\")\n          .when().post(\"\u002Fapi\u002Fdocuments\")\n          .then()\n          .statusCode(400);\n    }\n  }\n\n  private Document createDocument(Long id, String referenceNumber) {\n    Document document = new Document();\n    document.setId(id);\n    document.setReferenceNumber(referenceNumber);\n    document.setStatus(DocumentStatus.PENDING);\n    return document;\n  }\n}\n```\n\n## Integration Tests with Real Database\n\n```java\n@QuarkusTest\n@TestProfile(IntegrationTestProfile.class)\n@DisplayName(\"Document Integration Tests\")\nclass DocumentIntegrationTest {\n\n  @Test\n  @Transactional\n  @DisplayName(\"Should create and retrieve document via API\")\n  void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {\n    \u002F\u002F ACT - Create via API\n    Long id = given()\n        .contentType(ContentType.JSON)\n        .body(\"\"\"\n            {\n              \"referenceNumber\": \"INT-001\",\n              \"description\": \"Integration test\",\n              \"validUntil\": \"2030-01-01T00:00:00Z\",\n              \"categories\": [\"test\"]\n            }\n            \"\"\")\n        .when().post(\"\u002Fapi\u002Fdocuments\")\n        .then()\n        .statusCode(201)\n        .extract().path(\"id\");\n\n    \u002F\u002F ASSERT - Retrieve via API\n    given()\n        .when().get(\"\u002Fapi\u002Fdocuments\u002F\" + id)\n        .then()\n        .statusCode(200)\n        .body(\"referenceNumber\", equalTo(\"INT-001\"));\n  }\n}\n```\n\n## Coverage with JaCoCo\n\n### Maven Configuration (Complete)\n\n```xml\n\u003Cplugin>\n  \u003CgroupId>org.jacoco\u003C\u002FgroupId>\n  \u003CartifactId>jacoco-maven-plugin\u003C\u002FartifactId>\n  \u003Cversion>0.8.13\u003C\u002Fversion>\n  \u003Cexecutions>\n    \u003C!-- Prepare agent for test execution -->\n    \u003Cexecution>\n      \u003Cid>prepare-agent\u003C\u002Fid>\n      \u003Cgoals>\n        \u003Cgoal>prepare-agent\u003C\u002Fgoal>\n      \u003C\u002Fgoals>\n    \u003C\u002Fexecution>\n    \n    \u003C!-- Generate coverage report -->\n    \u003Cexecution>\n      \u003Cid>report\u003C\u002Fid>\n      \u003Cphase>verify\u003C\u002Fphase>\n      \u003Cgoals>\n        \u003Cgoal>report\u003C\u002Fgoal>\n      \u003C\u002Fgoals>\n    \u003C\u002Fexecution>\n    \n    \u003C!-- Enforce coverage thresholds -->\n    \u003Cexecution>\n      \u003Cid>check\u003C\u002Fid>\n      \u003Cgoals>\n        \u003Cgoal>check\u003C\u002Fgoal>\n      \u003C\u002Fgoals>\n      \u003Cconfiguration>\n        \u003Crules>\n          \u003Crule>\n            \u003Celement>BUNDLE\u003C\u002Felement>\n            \u003Climits>\n              \u003Climit>\n                \u003Ccounter>LINE\u003C\u002Fcounter>\n                \u003Cvalue>COVEREDRATIO\u003C\u002Fvalue>\n                \u003Cminimum>0.80\u003C\u002Fminimum>\n              \u003C\u002Flimit>\n              \u003Climit>\n                \u003Ccounter>BRANCH\u003C\u002Fcounter>\n                \u003Cvalue>COVEREDRATIO\u003C\u002Fvalue>\n                \u003Cminimum>0.70\u003C\u002Fminimum>\n              \u003C\u002Flimit>\n            \u003C\u002Flimits>\n          \u003C\u002Frule>\n        \u003C\u002Frules>\n      \u003C\u002Fconfiguration>\n    \u003C\u002Fexecution>\n  \u003C\u002Fexecutions>\n\u003C\u002Fplugin>\n```\n\nRun tests with coverage:\n```bash\nmvn clean test\nmvn jacoco:report\nmvn jacoco:check\n\n# Report at: target\u002Fsite\u002Fjacoco\u002Findex.html\n```\n\n## Test Dependencies\n\n```xml\n\u003Cdependencies>\n    \u003C!-- Quarkus Testing -->\n    \u003Cdependency>\n        \u003CgroupId>io.quarkus\u003C\u002FgroupId>\n        \u003CartifactId>quarkus-junit5\u003C\u002FartifactId>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n    \u003Cdependency>\n        \u003CgroupId>io.quarkus\u003C\u002FgroupId>\n        \u003CartifactId>quarkus-junit5-mockito\u003C\u002FartifactId>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n    \n    \u003C!-- Mockito -->\n    \u003Cdependency>\n        \u003CgroupId>org.mockito\u003C\u002FgroupId>\n        \u003CartifactId>mockito-core\u003C\u002FartifactId>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n    \n    \u003C!-- AssertJ (preferred over JUnit assertions) -->\n    \u003Cdependency>\n        \u003CgroupId>org.assertj\u003C\u002FgroupId>\n        \u003CartifactId>assertj-core\u003C\u002FartifactId>\n        \u003Cversion>3.24.2\u003C\u002Fversion>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n    \n    \u003C!-- REST Assured -->\n    \u003Cdependency>\n        \u003CgroupId>io.rest-assured\u003C\u002FgroupId>\n        \u003CartifactId>rest-assured\u003C\u002FartifactId>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n    \n    \u003C!-- Camel Testing -->\n    \u003Cdependency>\n        \u003CgroupId>org.apache.camel.quarkus\u003C\u002FgroupId>\n        \u003CartifactId>camel-quarkus-junit5\u003C\u002FartifactId>\n        \u003Cscope>test\u003C\u002Fscope>\n    \u003C\u002Fdependency>\n\u003C\u002Fdependencies>\n```\n\n## Best Practices\n\n### Test Organization\n- Use `@Nested` classes to group tests by method being tested\n- Use `@DisplayName` for readable test descriptions visible in reports\n- Follow `givenX_whenY_thenZ` naming convention for test methods\n- Use `@BeforeEach` for common test data setup to reduce duplication\n\n### Test Structure\n- Follow AAA pattern with explicit comments (`\u002F\u002F ARRANGE`, `\u002F\u002F ACT`, `\u002F\u002F ASSERT`)\n- Use `assertDoesNotThrow` for success scenarios\n- Use `assertThrows` for exception scenarios with message validation\n- Verify exception messages match expected values using AssertJ `contains()` or `isEqualTo()`\n\n### Test Coverage\n- Test happy paths for all public methods\n- Test null input handling\n- Test edge cases (empty collections, boundary values, negative IDs, blank strings)\n- Test exception scenarios comprehensively\n- Mock all external dependencies (repositories, services, Camel endpoints)\n- Aim for 80%+ line coverage, 70%+ branch coverage\n\n### Assertions\n- **Prefer AssertJ** (`assertThat`) over JUnit assertions for value checks\n- Use fluent AssertJ API for readability: `assertThat(list).hasSize(3).contains(item)`\n- For exceptions: use JUnit `assertThrows` to capture, then AssertJ to validate the message\n- For non-throwing success paths: use JUnit `assertDoesNotThrow`\n- For collections: `extracting()`, `filteredOn()`, `containsExactly()`\n\n### Testing Integration\n- Use `@QuarkusTest` for integration tests\n- Use `@InjectMock` to mock dependencies in Quarkus tests\n- Prefer REST Assured for API testing\n- Use `@TestProfile` for test-specific configuration\n\n### Event-Driven Testing\n- Test Camel routes with `AdviceWith` and `MockEndpoint`\n- Use `@CamelQuarkusTest` annotation (if using standalone Camel tests)\n- Verify message content, headers, and routing logic\n- Test error handling routes separately\n- Mock external systems (RabbitMQ, S3, databases) in unit tests\n\n### Camel Route Testing\n- Use `MockEndpoint` for asserting message flow\n- Use `AdviceWith` to modify routes for testing (replace endpoints with mocks)\n- Test message transformation and marshalling\n- Test exception handling and dead letter queues\n\n### Testing Async Operations\n- Test CompletableFuture success and failure scenarios\n- Use `.join()` in tests to wait for async completion\n- Test exception propagation from CompletableFuture\n- Verify LogContext propagation to async operations\n\n### Performance\n- Keep tests fast and isolated\n- Run tests in continuous mode: `mvn quarkus:test`\n- Use parameterized tests (`@ParameterizedTest`) for input variations\n- Build reusable test data builders or factory methods\n\n### Quarkus-Specific\n- Stay on latest LTS version (Quarkus 3.x)\n- Test native compilation compatibility periodically\n- Use Quarkus test profiles for different scenarios\n- Leverage Quarkus dev services for local testing\n- Use `@InjectMock` instead of `@MockBean` (Quarkus-specific)\n\n### Verification Best Practices\n- Always verify interactions on mocked dependencies\n- Use `verify(mock, never())` to ensure methods are NOT called in error scenarios\n- Use `argThat()` for complex argument matching\n- Verify the order of calls when it matters: `InOrder` from Mockito\n",{"name":5,"origin":48,"description":7},"ECC","master","8ec37df6d71fb664ea19d32c079e7d3ff931a8e0","skills\u002Fecc\u002Fquarkus-tdd",3,"https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit","https:\u002F\u002Fweblyfe.ai","https:\u002F\u002Fgithub.com\u002FS3YED","2026-08-27 01:54:02",{"source":8,"skillName":5,"commands":58,"prompt":68,"zipAvailable":69},[59,62,65],{"tool":60,"command":61},"npx","npx skills add S3YED\u002Fappie-kit --skill quarkus-tdd",{"tool":63,"command":64},"bunx","bunx skills add S3YED\u002Fappie-kit --skill quarkus-tdd",{"tool":66,"command":67},"pnpm","pnpm dlx skills add S3YED\u002Fappie-kit --skill quarkus-tdd","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fecc\u002Fquarkus-tdd\u002FSKILL.md",true,[],[72,104,139,165,195,218],{"skillNo":73,"name":74,"nameZh":6,"description":75,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":76,"categories":77,"keywords":85,"publishedAt":43},"01M6JFD4M6N4DSGC7TXEJ5R3J2","ai-music","AI music generation and audio tools: HeartMuLa lyrics-to-music generation, songwriting craft (+ Suno prompts), and audio spectrogram\u002Ffeature visualization.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fwethlete\u002Fai-music\u002FSKILL.md",[78,81,84],{"categoryNo":79,"categoryName":80,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000013","分析与可视化",{"categoryNo":82,"categoryName":83,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000016","图像与多媒体",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},[86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103],"music","ai music","humanizer","visualization","Tools","Audio","hermes","hermes agent","songwriting","suno","lyrics","prompts","heartmula","craft","generation","feature","ai music generation","music generation",{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":108,"categories":109,"keywords":118,"publishedAt":43},"01M6JFCZE4XNFQFMNZDHKHT5V5","agentic-webdesign-design-system-ingestion","Use v0 Design Systems 2.0, Bolt Design System Agents, or Lovable to generate production UI from a client's real design system components — not generic Tailwind. Use when building a client website where brand consistency matters.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fweb-design\u002Fagentic-webdesign-design-system-ingestion\u002FSKILL.md",[110,113],{"categoryNo":111,"categoryName":112,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":114,"categoryName":115,"taxonomyType":116,"taxonomyTypeDesc":117},"01JBSEEDCAT000000000000032","设计师","PROFESSION","适用职业",[119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138],"design","agentic","design system","system","ingestion","webdesign","agentic webdesign design system ingestion","components","generate","design systems","website","ui","Tailwind","bolt","lovable","v0","design-system","wed","design system components","appie",{"skillNo":140,"name":141,"nameZh":6,"description":142,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":143,"categories":144,"keywords":147,"publishedAt":43},"01M6JFCVPX3913CFAS6QQJ33ZB","agentic-webdesign-agent-elements","Use 21st.dev Agent Elements — 25 shadcn\u002Fui components purpose-built for AI agent UIs. Tool cards (Bash, Edit, Search, Plan, Subagent, MCP), chat, input, streaming. Open source, shadcn-registry install, no runtime lock-in.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fweb-design\u002Fagentic-webdesign-agent-elements\u002FSKILL.md",[145,146],{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":114,"categoryName":115,"taxonomyType":116,"taxonomyTypeDesc":117},[120,148,124,149,150,151,126,152,153,154,155,156,157,158,159,160,161,162,163,164],"elements","agentic webdesign agent elements","MCP","ai agent","registry","search","streaming","open source","open","Bash","plan","subagent","edit","install","input","lock","source",{"skillNo":166,"name":167,"nameZh":6,"description":168,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":169,"categories":170,"keywords":175,"publishedAt":43},"01M6JFCQC7H65M9QJ6TRE71BS1","vercel-deployment","Deploy static sites and frontends to Vercel — handles both CLI and REST API paths, including the CLI token sanitization workaround.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Fvercel-deployment\u002FSKILL.md",[171,174],{"categoryNo":172,"categoryName":173,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000011","运维与部署",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},[176,177,178,179,180,181,182,183,184,32,185,186,187,188,189,190,191,192,193,194],"deployment","vercel deployment","vercel","macos","cli","linux","windows","deploy","vercel deploy","api","site","deploy to vercel","token","rest api","host","sites","paths","sanitization","static site",{"skillNo":196,"name":197,"nameZh":6,"description":198,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":199,"categories":200,"keywords":202,"publishedAt":43},"01M6JFCM4SMTXBM5A01BK2YQG1","software-quality-workflows","Umbrella workflow for TDD, debugging, code simplification, spikes, pre-commit review, and debugger-assisted investigation.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Fsoftware-quality-workflows\u002FSKILL.md",[201],{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},[203,204,205,206,207,26,208,209,210,211,212,213,214,215,216,217,39],"workflows","quality","software","software quality workflows","review","workflow","commit","debugging","code","debugger","pre-commit","investigation","code simplification","user","simplification",{"skillNo":219,"name":220,"nameZh":6,"description":221,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":222,"categories":223,"keywords":231,"publishedAt":43},"01M6JFCGD0M6CEATY01W7YN6DB","react-component-refactoring","Safely refactor large React components into smaller, maintainable pieces using proven patterns","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Freact-component-refactoring\u002FSKILL.md",[224,227,230],{"categoryNo":225,"categoryName":226,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000007","代码审查",{"categoryNo":228,"categoryName":229,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},[232,233,34,234,235,236,237,238,179,239,240,126,241,182,242,211,243,244,245,246],"react","component","component refactoring","react component","react component refactoring","frontend","writing","safe","typescript","react components","modular","refactor","simplify","patterns","test-driven-development"]