Communitygithub.com

quarkus-tdd

用于 Quarkus 3.x LTS 的测试驱动开发,结合 JUnit 5、Mockito、REST Assured、Camel 测试和 JaCoCo。在添加功能、修复 Bug 或重构事件驱动服务时使用。

quarkus-tdd 是什么?

该技能为 Quarkus 3.x 服务提供了一个结构化的测试驱动开发 (TDD) 工作流程,重点在于实现 80% 以上的代码覆盖率(包括单元测试和集成测试)。它专门针对使用 Apache Camel 的事件驱动架构进行了优化。主要功能包括使用 @Nested 和 @DisplayName 注释组织清晰的单元测试、使用 AdviceWith 进行 Apache Camel 路由测试、用于消息驱动的 RabbitMQ 集成测试、CompletableFuture 异步操作验证、LogContext 传播测试以及使用 JaCoCo 进行覆盖率强制执行。该技能适用于 Cursor、Google IDX、Replit、Windsurf 和 Lovable 等 AI 编码代理,为开发者提供了一种一致的测试事件驱动业务规则、数据访问层和安全约束的方法。其工作流程遵循标准的红/绿/重构循环,确保代码质量和可靠性。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/quarkus-tdd

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Quarkus TDD 工作流程

面向 Quarkus 3.x 服务的 TDD 指导,覆盖率达 80% 以上(单元测试 + 集成测试)。专为使用 Apache Camel 的事件驱动架构优化。

使用时机

  • 新功能或 REST 端点
  • Bug 修复或重构
  • 添加数据访问逻辑、安全规则或响应式流
  • 测试 Apache Camel 路由和事件处理器
  • 使用 RabbitMQ 测试事件驱动服务
  • 测试条件流逻辑
  • 验证 CompletableFuture 异步操作
  • 测试 LogContext 传播

工作流程

  1. 先编写测试(它们应该失败)
  2. 实现最小化代码以通过测试
  3. 在测试通过后重构
  4. 使用 JaCoCo 强制执行覆盖率(80% 以上目标)

使用 @Nested 组织进行单元测试

遵循以下结构化的方法,编写全面且可读的测试:

@ExtendWith(MockitoExtension.class)
@DisplayName("OrderService 单元测试")
class OrderServiceTest {

  @Mock
  private OrderRepository orderRepository;

  @Mock
  private EventService eventService;

  @Mock
  private FulfillmentPublisher fulfillmentPublisher;

  @InjectMocks
  private OrderService orderService;

  private CreateOrderCommand validCommand;

  @BeforeEach
  void setUp() {
    validCommand = new CreateOrderCommand(
        "customer-123",
        List.of(new OrderLine("sku-123", 2))
    );
  }

  @Nested
  @DisplayName("createOrder 测试")
  class CreateOrder {

    @Test
    @DisplayName("应持久化订单并发布履约事件")
    void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {
      // 准备
      doNothing().when(orderRepository).persist(any(Order.class));

      // 执行
      OrderReceipt receipt = orderService.createOrder(validCommand);

      // 断言
      assertThat(receipt).isNotNull();
      assertThat(receipt.customerId()).isEqualTo("customer-123");
      verify(orderRepository).persist(any(Order.class));
      verify(fulfillmentPublisher).publishAsync(receipt);
      verify(eventService).createSuccessEvent(receipt, "ORDER_CREATED");
    }

    @Test
    @DisplayName("应拒绝缺少客户 ID 的请求")
    void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {
      // 准备
      CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());

      // 执行与断言
      WebApplicationException exception = assertThrows(
          WebApplicationException.class,
          () -> orderService.createOrder(invalid)
      );

      assertThat(exception.getResponse().getStatus()).isEqualTo(400);
      verify(orderRepository, never()).persist(any(Order.class));
      verify(fulfillmentPublisher, never()).publishAsync(any());
    }

    @Test
    @DisplayName("持久化失败时应记录错误事件")
    void givenPersistenceFailure_whenCreateOrder_thenRecordsErrorEvent() {
      // 准备
      doThrow(new PersistenceException("database unavailable"))
          .when(orderRepository).persist(any(Order.class));

      // 执行与断言
      PersistenceException exception = assertThrows(
          PersistenceException.class,
          () -> orderService.createOrder(validCommand)
      );

      assertThat(exception.getMessage()).contains("database unavailable");
      verify(eventService).createErrorEvent(
          eq(validCommand),
          eq("ORDER_CREATE_FAILED"),
          contains("database unavailable")
      );
      verify(fulfillmentPublisher, never()).publishAsync(any());
    }

    @Test
    @DisplayName("应拒绝空命令")
    void givenNullCommand_whenCreateOrder_thenThrowsNullPointerException() {
      // 执行与断言
      assertThrows(
          NullPointerException.class,
          () -> orderService.createOrder(null)
      );

      verify(orderRepository, never()).persist(any(Order.class));
    }
  }
}

关键测试模式

  1. @Nested 类:按测试方法分组
  2. @DisplayName:为测试报告提供可读的测试描述
  3. 命名约定:givenX_whenY_thenZ 确保清晰
  4. AAA 模式:显式的 // 准备、// 执行、// 断言 注释
  5. @BeforeEach:设置通用测试数据以减少重复
  6. assertDoesNotThrow:测试成功场景,无需捕获异常
  7. assertThrows:使用 AssertJ 测试异常场景并验证消息
  8. 全面覆盖:测试快乐路径、空输入、边界情况、异常
  9. 验证交互:使用 Mockito verify() 确保方法被正确调用
  10. 绝不验证:使用 never() 确保在错误场景中方法未被调用

测试 Camel 路由

@QuarkusTest
@DisplayName("业务规则 Camel 路由测试")
class BusinessRulesRouteTest {

  @Inject
  CamelContext camelContext;

  @Inject
  ProducerTemplate producerTemplate;

  @InjectMock
  EventService eventService;

  @InjectMock
  DocumentValidator documentValidator;

  private BusinessRulesPayload testPayload;

  @BeforeEach
  void setUp() {
    // 准备 - 测试数据
    testPayload = new BusinessRulesPayload();
    testPayload.setDocumentId(1L);
    testPayload.setFlowProfile(FlowProfile.BASIC);
  }

  @Nested
  @DisplayName("business-rules-publisher 路由测试")
  class BusinessRulesPublisher {

    @Test
    @DisplayName("应成功将消息发布到 RabbitMQ")
    void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
      // 准备
      MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
      mockRabbitMQ.expectedMessageCount(1);

      // 为测试替换真实端点
      camelContext.getRouteController().stopRoute("business-rules-publisher");
      AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
        advice.replaceFromWith("direct:business-rules-publisher");
        advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
      });
      camelContext.getRouteController().startRoute("business-rules-publisher");

      // 执行
      producerTemplate.sendBody("direct:business-rules-publisher", testPayload);

      // 断言 — body 是 .marshal().json(JsonLibrary.Jackson) 后的 JSON 字符串
      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 序列化")
    void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {
      // 准备
      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");

      // 执行
      producerTemplate.sendBody("direct:business-rules-publisher", testPayload);

      // 断言
      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 路由测试")
  class DocumentProcessing {

    @Test
    @DisplayName("应路由发票到正确的处理器")
    void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {
      // 准备
      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");

      // 执行
      producerTemplate.sendBodyAndHeader("direct:process-document",
          testPayload, "documentType", "INVOICE");

      // 断言
      mockInvoice.assertIsSatisfied(5000);
    }

    @Test
    @DisplayName("应优雅处理验证错误")
    void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {
      // 准备
      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");

      // Mock 验证器 bean 抛出异常
      when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document"));

      // 执行
      producerTemplate.sendBody("direct:process-document", testPayload);

      // 断言
      mockError.assertIsSatisfied(5000);

      Exception exception = mockError.getExchanges().get(0).getException();
      assertThat(exception).isInstanceOf(ValidationException.class);
      assertThat(exception.getMessage()).contains("Invalid document");
    }
  }
}

测试事件服务

@ExtendWith(MockitoExtension.class)
@DisplayName("EventService 单元测试")
class EventServiceTest {

  @Mock
  private EventRepository eventRepository;

  @Mock
  private ObjectMapper objectMapper;

  @InjectMocks
  private EventService eventService;

  private BusinessRulesPayload testPayload;

  @BeforeEach
  void setUp() {
    // 准备
    testPayload = new BusinessRulesPayload();
    testPayload.setDocumentId(1L);
  }

  @Nested
  @DisplayName("createSuccessEvent 测试")
  class CreateSuccessEvent {

    @Test
    @DisplayName("应使用正确属性创建成功事件")
    void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
      // 准备
      when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");

      // 执行
      assertDoesNotThrow(() ->
          eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));

      // 断言
      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 为空时应抛出异常")
    void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
      // 准备
      Object nullPayload = null;

      // 执行与断言
      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 测试")
  class CreateErrorEvent {

    @Test
    @DisplayName("应创建包含错误消息的错误事件")
    void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
      // 准备
      String errorMessage = "Processing failed";
      when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");

      // 执行
      assertDoesNotThrow(() ->
          eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));

      // 断言
      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("应拒绝无效的错误消息")
    @ValueSource(strings = {"", " "})
    void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {
      // 执行与断言
      IllegalArgumentException exception = assertThrows(
          IllegalArgumentException.class,
          () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
      );

      assertThat(exception.getMessage()).contains("Error message cannot be blank");
    }
  }
}

测试 CompletableFuture

@ExtendWith(MockitoExtension.class)
@DisplayName("FileStorageService 单元测试")
class FileStorageServiceTest {

  @Mock
  private S3Client s3Client;

  @Mock
  private ExecutorService executorService;

  @InjectMocks
  private FileStorageService fileStorageService;

  private InputStream testInputStream;
  private LogContext testLogContext;

  @BeforeEach
  void setUp() {
    // 准备
    testInputStream = new ByteArrayInputStream("test content".getBytes());
    testLogContext = new LogContext();
    testLogContext.put("traceId", "trace-123");
  }

  @Nested
  @DisplayName("uploadOriginalFile 测试")
  class UploadOriginalFile {

    @Test
    @DisplayName("应成功上传文件并返回文档信息")
    void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
      // 准备
      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());

      // 执行
      CompletableFuture<StoredDocumentInfo> future

…

[截断以便翻译]

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

相关技能