Communitygithub.com

quarkus-tdd

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

quarkus-tdd 是什么?

该技能为 Quarkus 3.x LTS 服务提供测试驱动开发(TDD)的全面指导,目标覆盖 80% 以上的测试覆盖率(单元测试 + 集成测试)。它针对使用 Apache Camel 的事件驱动架构进行了优化。技能支持多种测试场景,包括新功能或 REST 端点、缺陷修复或重构、数据访问逻辑、安全规则、响应式流、Camel 路由、事件处理器以及异步操作(如 CompletableFuture)。主要工作流程遵循经典的 TDD 循环:先编写测试(预期失败),然后实现最小化代码通过测试,最后在测试通过后进行重构。使用 JaCoCo 强制保持 80% 以上的覆盖率。技能提供了详细的代码示例,展示了使用 @Nested 类的结构化单元测试、使用 AdviceWith 的 Camel 路由测试、事件服务测试以及 CompletableFuture 异步操作的测试。它还涵盖了关键测试模式,如 AAA(Arrange-Act-Assert)模式、Mockito 的 verify() 和 never() 验证、以及异常场景测试。该技能适用于所有主流 AI 编码代理平台,如 Claude Code、Cursor 和 Codex。

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

在你喜欢的 AI 中提问

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

文档

Quarkus TDD 工作流程

面向覆盖率达到 80% 以上(单元测试 + 集成测试)的 Quarkus 3.x 服务的 TDD 指南。针对使用 Apache Camel 的事件驱动架构进行了优化。

适用场景

  • 新功能或 REST 端点
  • 缺陷修复或重构
  • 添加数据访问逻辑、安全规则或响应式流
  • 测试 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() {
      // 准备(ARRANGE)
      doNothing().when(orderRepository).persist(any(Order.class));

      // 操作(ACT)
      OrderReceipt receipt = orderService.createOrder(validCommand);

      // 断言(ASSERT)
      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() {
      // 准备(ARRANGE)
      CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());

      // 操作与断言(ACT & ASSERT)
      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() {
      // 准备(ARRANGE)
      doThrow(new PersistenceException("数据库不可用"))
          .when(orderRepository).persist(any(Order.class));

      // 操作与断言(ACT & ASSERT)
      PersistenceException exception = assertThrows(
          PersistenceException.class,
          () -> orderService.createOrder(validCommand)
      );

      assertThat(exception.getMessage()).contains("数据库不可用");
      verify(eventService).createErrorEvent(
          eq(validCommand),
          eq("ORDER_CREATE_FAILED"),
          contains("数据库不可用")
      );
      verify(fulfillmentPublisher, never()).publishAsync(any());
    }

    @Test
    @DisplayName("应该拒绝空命令")
    void givenNullCommand_whenCreateOrder_thenThrowsNullPointerException() {
      // 操作与断言(ACT & ASSERT)
      assertThrows(
          NullPointerException.class,
          () -> orderService.createOrder(null)
      );

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

关键测试模式

  1. @Nested 类:按被测试方法分组测试
  2. @DisplayName:为测试报告提供可读的测试描述
  3. 命名约定:givenX_whenY_thenZ 提高清晰度
  4. AAA 模式:显式使用 // 准备(ARRANGE)、// 操作(ACT)、// 断言(ASSERT) 注释
  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() {
    // 准备(ARRANGE)- 测试数据
    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 {
      // 准备(ARRANGE)
      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");

      // 操作(ACT)
      producerTemplate.sendBody("direct:business-rules-publisher", testPayload);

      // 断言(ASSERT)— 经过 .marshal().json(JsonLibrary.Jackson) 后,body 是 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 {
      // 准备(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 路由测试")
  class DocumentProcessing {

    @Test
    @DisplayName("应将发票路由到正确的处理器")
    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("应优雅地处理验证错误")
    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");

      // 模拟验证 Bean 抛出异常
      when(documentValidator.validate(any())).thenThrow(new ValidationException("无效文档"));

      // 操作(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("无效文档");
    }
  }
}

测试事件服务

@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() {
    // 准备(ARRANGE)
    testPayload = new BusinessRulesPayload();
    testPayload.setDocumentId(1L);
  }

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

    @Test
    @DisplayName("应创建具有正确属性的成功事件")
    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 为空时应抛出异常")
    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 不能为空");
      verify(eventRepository, never()).persist(any());
    }
  }

  @Nested
  @DisplayName("createErrorEvent 测试")
  class CreateErrorEvent {

    @Test
    @DisplayName("应创建包含错误消息的错误事件")
    void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
      // 准备(ARRANGE)
      String errorMessage = "处理失败";
      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("应拒绝无效的错误消息")
    @ValueSource(strings = {"", " "})
    void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {
      // 操作与断言(ACT & ASSERT)
      IllegalArgumentException exception = assertThrows(
          IllegalArgumentException.class,
          () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
      );

      assertThat(exception.getMessage()).contains("错误消息不能为空");
    }
  }
}

测试 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() {
    // 准备(ARRANGE)
    testInputStream = new ByteArrayInputStream("测试内容".getBytes());
    testLogContext = new LogContext();
    testLogContext.put("traceId", "trace-123");
  }

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

    @Test
    @DisplayName("应成功上传文件并返回文档信息")
    void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
      // 准备(ARRANGE)
      doAnswer(invocation -> {
        ((Runnable) invocation.getArgument(0)).run();
        return null;
      }).when(executorService).execute(any(Runnable.class));

      when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
          .thenReturn(PutObjectResponse.builder().build());

      // 操作(ACT)
      CompletableFuture<StoredDocumentInfo> future

…[已截断以进行翻译]

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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能