Communitygithub.com

Quarkus TDD

Quarkus 3.x LTS向けテスト駆動開発。JUnit 5、Mockito、REST Assured、Camelテスト、JaCoCoを使用。機能追加、バグ修正、イベント駆動サービスのリファクタリング時に使用。

Quarkus TDD とは?

このスキルは、Quarkus 3.x LTSサービス向けにテスト駆動開発(TDD)を実践するための包括的なガイドを提供します。主にApache Camelを活用したイベント駆動型アーキテクチャに最適化されており、ユニットテストと統合テストを組み合わせて80%以上のコードカバレッジを達成することを目標としています。JUnit 5、Mockito、REST Assuredなどの主要なテストフレームワークと、Apache Camelルートのテスト方法、イベントサービスのテスト、CompletableFutureを用いた非同期処理のテストパターンが豊富に含まれています。JaCoCoを使用してコードカバレッジを計測・強制し、リファクタリング時の品質保証にも貢献します。このスキルは、機能追加、バグ修正、リファクタリングなど、あらゆる開発フェーズで活用でき、特にRabbitMQを使用したメッセージングやCamelルートを含む複雑なビジネスロジックのテストに威力を発揮します。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/quarkus-tdd

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Quarkus TDD ワークフロー

Quarkus 3.x サービス向け、80%以上のカバレッジ(ユニット+統合)を目標とした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() {
      // 準備
      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("nullコマンドは拒否する")
    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. 包括的カバレッジ: 正常系、null入力、エッジケース、例外をテストする
  9. 相互作用の検証: Mockito verify() を使用してメソッドが正しく呼び出されることを確認する
  10. Never検証: 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);

      // 検証 — .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(" invoice を正しいプロセッサにルーティングする")
    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");

      // バリデータ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("ペイロードがnullの場合は例外をスローする")
    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

…

[truncated for translation]

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.

関連スキル