Source profileQuality 89/100

affaan-m/ECC/docs/ja-JP/skills/quarkus-tdd/SKILL.md

quarkus-tdd

Use it for testing and engineering tasks; the detail page covers purpose, installation, and practical steps.

Source repository stars
234,327
Declared platforms
0
Static risk flags
0
Last source update
2026-07-27
Source checked
2026-07-28

Decision brief

What it does—and where it fits

80%以上のカバレッジ(ユニット+統合)を備えたQuarkus 3.xサービスのTDD指導。Apache Camelを使用したイベント駆動アーキテクチャに最適化。

Best for

  • 新機能またはRESTエンドポイント
  • バグ修正またはリファクタリング
  • データアクセスロジック、セキュリティルール、またはリアクティブストリーム追加

Not for

  • Tasks that require unconfirmed production actions or broad system permissions.
  • Environments where the pinned source and install steps cannot be inspected.

Compatibility matrix

Platform support, with evidence labels

PlatformStatusEvidenceWhat to check
CodexNot declaredNo explicit evidencePortability before use
Claude CodeNot declaredNo explicit evidencePortability before use
CursorNot declaredNo explicit evidencePortability before use
Gemini CLINot declaredNo explicit evidencePortability before use
Open the compatibility checker

Installation

Inspect first. Install second.

The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

Source-detected install commandSource
npx skills add https://github.com/affaan-m/ECC --skill "docs/ja-JP/skills/quarkus-tdd"
Safe inspection promptEditorial

Inspect the Agent Skill "quarkus-tdd" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/docs/ja-JP/skills/quarkus-tdd/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

Workflow

What the source asks the agent to do

  1. 01

    Workflow

    1. テストを先に書く(失敗するはず) 2. 最小限のコードで合格実装 3. テストが緑の状態でリファクタリング 4. JaCoCoでカバレッジ実装(80%以上を目標)

    テストを先に書く(失敗するはず)最小限のコードで合格実装テストが緑の状態でリファクタリング
  2. 02

    When to Use

    新機能またはRESTエンドポイント

    新機能またはRESTエンドポイントバグ修正またはリファクタリングデータアクセスロジック、セキュリティルール、またはリアクティブストリーム追加
  3. 03

    Unit Tests with @Nested Organization

    包括的で読みやすいテストのため、以下の構造化されたアプローチに従います:

    @Nested クラス: テストするメソッド別にテストをグループ化@DisplayName: テストレポート用の読みやすい説明提供命名規則: 明確性のため givenXwhenYthenZ
  4. 04

    Key Testing Patterns

    1. @Nested クラス: テストするメソッド別にテストをグループ化 2. @DisplayName: テストレポート用の読みやすい説明提供 3. 命名規則: 明確性のため givenXwhenYthenZ 4. AAA パターン: 明示的な // ARRANGE, // ACT, // ASSERT コメント 5. @BeforeEach: 重複削減のためテストデータを共通設定 6. assertDoesNotThrow: 例外をキャッチせずに成功シナリオをテスト 7. assertThrows: AssertJを使用したメッセージ検証で例外シナ…

    @Nested クラス: テストするメソッド別にテストをグループ化@DisplayName: テストレポート用の読みやすい説明提供命名規則: 明確性のため givenXwhenYthenZ
  5. 05

    Testing Camel Routes

    Review the “Testing Camel Routes” section in the pinned source before continuing.

    Review and apply the “Testing Camel Routes” source section.

Permission review

Static risk signals and limitations

No configured static risk pattern was detected

This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.

Evidence record

Why each signal appears

EvidenceSourceComputedTestedEditorial
SignalValueEvidence typeMeaning
Quality score89/100ComputedDocumentation, specificity, maintenance, and trust rules
Repository stars234,327SourceRepository attention, not individual Skill quality
Compatibility0 platformsSourceDeclared in the catalog source record
Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

Pinned source

Provenance and original SKILL.md

Repository
affaan-m/ECC
Skill path
docs/ja-JP/skills/quarkus-tdd/SKILL.md
Commit
4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
License
MIT
Collected
2026-07-28
Default branch
main
View the original SKILL.md

Quarkus TDD Workflow

80%以上のカバレッジ(ユニット+統合)を備えたQuarkus 3.xサービスのTDD指導。Apache Camelを使用したイベント駆動アーキテクチャに最適化。

When to Use

  • 新機能またはRESTエンドポイント
  • バグ修正またはリファクタリング
  • データアクセスロジック、セキュリティルール、またはリアクティブストリーム追加
  • Apache Camelルートとイベントハンドラーテスト
  • RabbitMQを使用したイベント駆動サービステスト
  • 条件フローロジック検証
  • CompletableFuture非同期操作検証
  • LogContextプロパゲーション テスト

Workflow

  1. テストを先に書く(失敗するはず)
  2. 最小限のコードで合格実装
  3. テストが緑の状態でリファクタリング
  4. JaCoCoでカバレッジ実装(80%以上を目標)

Unit Tests with @Nested Organization

包括的で読みやすいテストのため、以下の構造化されたアプローチに従います:

@ExtendWith(MockitoExtension.class)
@DisplayName("OrderService Unit Tests")
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が無い場合、BadRequestをスロー")
    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("database unavailable"))
          .when(orderRepository).persist(any(Order.class));

      // ACT & ASSERT
      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コマンドが与えられた場合、NullPointerExceptionをスロー")
    void givenNullCommand_whenCreateOrder_thenThrowsNullPointerException() {
      // ACT & ASSERT
      assertThrows(
          NullPointerException.class,
          () -> orderService.createOrder(null)
      );

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

Key Testing Patterns

  1. @Nested クラス: テストするメソッド別にテストをグループ化
  2. @DisplayName: テストレポート用の読みやすい説明提供
  3. 命名規則: 明確性のため givenX_whenY_thenZ
  4. AAA パターン: 明示的な // ARRANGE, // ACT, // ASSERT コメント
  5. @BeforeEach: 重複削減のためテストデータを共通設定
  6. assertDoesNotThrow: 例外をキャッチせずに成功シナリオをテスト
  7. assertThrows: AssertJを使用したメッセージ検証で例外シナリオをテスト
  8. 包括的カバレッジ: 正常系、null入力、エッジケース、例外をテスト
  9. 相互作用検証: Mockito verify() でメソッド呼び出しが正しく行われたか確認
  10. Never検証: never() でエラーシナリオでメソッドが呼ばれていないことを確認

Testing Camel Routes

@QuarkusTest
@DisplayName("Business Rules Camel Route Tests")
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");

      // バリデータビーンをモック化して例外をスロー
      when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document"));

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

      // ASSERT
      mockError.assertIsSatisfied(5000);

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

Testing Event Services

@ExtendWith(MockitoExtension.class)
@DisplayName("EventService Unit Tests")
class EventServiceTest {

  @Mock
  private EventRepository eventRepository;

  @Mock
  private ObjectMapper objectMapper;

  @InjectMocks
  private EventService eventService;

  private BusinessRulesPayload testPayload;

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

  @Nested
  @DisplayName("createSuccessEvent のテスト")
  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("nullペイロードが与えられた場合、例外をスロー")
    void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
      // ARRANGE
      Object nullPayload = null;

      // ACT & ASSERT
      NullPointerException exception = assertThrows(
          NullPointerException.class,
          () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
      );

      assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
      verify(eventRepository, never()).persist(any());
    }
  }

  @Nested
  @DisplayName("createErrorEvent のテスト")
  class CreateErrorEvent {

    @Test
    @DisplayName("エラーが与えられた場合、エラーメッセージ付きエラーイベント作成")
    void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {
      // ARRANGE
      String errorMessage = "Processing failed";
      when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");

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

      // ASSERT
      verify(eventRepository).persist(argThat(event ->
          event.getType().equals("PROCESSING_ERROR") &&
          event.getStatus() == EventStatus.ERROR &&
          event.getErrorMessage().equals(errorMessage) &&
          event.getPayload().equals("{\"documentId\":1}")
      ));
    }

    @ParameterizedTest
    @DisplayName("不正なエラーメッセージが与えられた場合、例外をスロー")
    @ValueSource(strings = {"", " "})
    void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {
      // ACT & ASSERT
      IllegalArgumentException exception = assertThrows(
          IllegalArgumentException.class,
          () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
      );

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

Testing CompletableFuture

@ExtendWith(MockitoExtension.class)
@DisplayName("FileStorageService Unit Tests")
class FileStorageServiceTest {

  @Mock
  private S3Client s3Client;

  @Mock
  private ExecutorService executorService;

  @InjectMocks
  private FileStorageService fileStorageService;

  private InputStream testInputStream;
  private LogContext testLogContext;

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

  @Nested
  @DisplayName("uploadOriginalFile のテスト")
  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 =
          fileStorageService.uploadOriginalFile(testInputStream, 1024L,
              testLogContext, InvoiceFormat.UBL);

      StoredDocumentInfo result = future.join();

      // ASSERT
      assertThat(result).isNotNull();
      assertThat(result.getPath()).isNotBlank();
      assertThat(result.getSize()).isEqualTo(1024L);
      assertThat(result.getUploadedAt()).isNotNull();

      verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
    }

    @Test
    @DisplayName("S3アップロード失敗が与えられた場合、CompletableFutureが失敗")
    void givenS3Failure_whenUpload_thenCompletableFutureFails() {
      // ARRANGE — 例外がfutureを通じてプロパゲートされるように同期実行
      doAnswer(invocation -> {
        ((Runnable) invocation.getArgument(0)).run();
        return null;
      }).when(executorService).execute(any(Runnable.class));

      when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
          .thenThrow(new StorageException("S3 unavailable"));

      // ACT
      CompletableFuture<StoredDocumentInfo> future =
          fileStorageService.uploadOriginalFile(testInputStream, 1024L,
              testLogContext, InvoiceFormat.UBL);

      // ASSERT
      assertThatThrownBy(() -> future.join())
          .isInstanceOf(CompletionException.class)
          .hasCauseInstanceOf(StorageException.class)
          .hasMessageContaining("S3 unavailable");
    }

    @Test
    @DisplayName("LogContextが与えられた場合、非同期操作にコンテキストをプロパゲート")
    void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
      // ARRANGE
      AtomicReference<LogContext> capturedContext = new AtomicReference<>();

      doAnswer(invocation -> {
        capturedContext.set(CustomLog.getCurrentContext());
        ((Runnable) invocation.getArgument(0)).run();
        return null;
      }).when(executorService).execute(any(Runnable.class));

      // ACT
      fileStorageService.uploadOriginalFile(testInputStream, 1024L,
          testLogContext, InvoiceFormat.UBL).join();

      // ASSERT
      assertThat(capturedContext.get()).isNotNull();
      assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
    }
  }
}

Resource Layer Tests (REST Assured)

@QuarkusTest
@DisplayName("DocumentResource API Tests")
class DocumentResourceTest {

  @InjectMock
  DocumentService documentService;

  @Nested
  @DisplayName("GET /api/documents のテスト")
  class ListDocuments {

    @Test
    @DisplayName("ドキュメントが存在する場合、ドキュメント一覧を返す")
    void givenDocumentsExist_whenList_thenReturnsOk() {
      // ARRANGE
      List<Document> documents = List.of(createDocument(1L, "DOC-001"));
      when(documentService.list(0, 20)).thenReturn(documents);

      // ACT & ASSERT
      given()
          .when().get("/api/documents")
          .then()
          .statusCode(200)
          .body("$.size()", is(1))
          .body("[0].referenceNumber", equalTo("DOC-001"));
    }
  }

  @Nested
  @DisplayName("POST /api/documents のテスト")
  class CreateDocument {

    @Test
    @DisplayName("有効なリクエストが与えられた場合、ドキュメント作成して201を返す")
    void givenValidRequest_whenCreate_thenReturns201() {
      // ARRANGE
      Document document = createDocument(1L, "DOC-001");
      when(documentService.create(any())).thenReturn(document);

      // ACT & ASSERT
      given()
          .contentType(ContentType.JSON)
          .body("""
              {
                "referenceNumber": "DOC-001",
                "description": "Test document",
                "validUntil": "2030-01-01T00:00:00Z",
                "categories": ["test"]
              }
              """)
          .when().post("/api/documents")
          .then()
          .statusCode(201)
          .header("Location", containsString("/api/documents/1"))
          .body("referenceNumber", equalTo("DOC-001"));
    }

    @Test
    @DisplayName("不正なリクエストが与えられた場合、400を返す")
    void givenInvalidRequest_whenCreate_thenReturns400() {
      // ACT & ASSERT
      given()
          .contentType(ContentType.JSON)
          .body("""
              {
                "referenceNumber": "",
                "description": "Test"
              }
              """)
          .when().post("/api/documents")
          .then()
          .statusCode(400);
    }
  }

  private Document createDocument(Long id, String referenceNumber) {
    Document document = new Document();
    document.setId(id);
    document.setReferenceNumber(referenceNumber);
    document.setStatus(DocumentStatus.PENDING);
    return document;
  }
}

Integration Tests with Real Database

@QuarkusTest
@TestProfile(IntegrationTestProfile.class)
@DisplayName("Document Integration Tests")
class DocumentIntegrationTest {

  @Test
  @Transactional
  @DisplayName("新規ドキュメントをAPIで作成・取得、成功する")
  void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {
    // ACT - APIで作成
    Long id = given()
        .contentType(ContentType.JSON)
        .body("""
            {
              "referenceNumber": "INT-001",
              "description": "Integration test",
              "validUntil": "2030-01-01T00:00:00Z",
              "categories": ["test"]
            }
            """)
        .when().post("/api/documents")
        .then()
        .statusCode(201)
        .extract().path("id");

    // ASSERT - APIで取得
    given()
        .when().get("/api/documents/" + id)
        .then()
        .statusCode(200)
        .body("referenceNumber", equalTo("INT-001"));
  }
}

Coverage with JaCoCo

Maven Configuration (Complete)

<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.13</version>
  <executions>
    <!-- テスト実行用エージェント準備 -->
    <execution>
      <id>prepare-agent</id>
      <goals>
        <goal>prepare-agent</goal>
      </goals>
    </execution>

    <!-- カバレッジレポート生成 -->
    <execution>
      <id>report</id>
      <phase>verify</phase>
      <goals>
        <goal>report</goal>
      </goals>
    </execution>

    <!-- カバレッジ閾値を強制 -->
    <execution>
      <id>check</id>
      <goals>
        <goal>check</goal>
      </goals>
      <configuration>
        <rules>
          <rule>
            <element>BUNDLE</element>
            <limits>
              <limit>
                <counter>LINE</counter>
                <value>COVEREDRATIO</value>
                <minimum>0.80</minimum>
              </limit>
              <limit>
                <counter>BRANCH</counter>
                <value>COVEREDRATIO</value>
                <minimum>0.70</minimum>
              </limit>
            </limits>
          </rule>
        </rules>
      </configuration>
    </execution>
  </executions>
</plugin>

カバレッジ付きテスト実行:

mvn clean test
mvn jacoco:report
mvn jacoco:check

# レポート: target/site/jacoco/index.html

Test Dependencies

<dependencies>
    <!-- Quarkus Testing -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5-mockito</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Mockito -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- AssertJ(JUnitアサーション推奨) -->
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.24.2</version>
        <scope>test</scope>
    </dependency>

    <!-- REST Assured -->
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Camel Testing -->
    <dependency>
        <groupId>org.apache.camel.quarkus</groupId>
        <artifactId>camel-quarkus-junit5</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Best Practices

テスト組織

  • テストするメソッド別にグループ化するため@Nestedクラス使用
  • レポートで見やすいテスト説明のため@DisplayName使用
  • テストメソッド命名はgivenX_whenY_thenZ規則に従う
  • 重複削減のため@BeforeEachで共通テストデータ設定

テスト構造

  • 明示的コメント(// ARRANGE, // ACT, // ASSERT)でAAAパターン従う
  • 成功シナリオではassertDoesNotThrow使用
  • 例外シナリオではメッセージ検証と共にassertThrows使用
  • AssertJ contains()またはisEqualTo()で例外メッセージ検証

テストカバレッジ

  • 全パブリックメソッドの正常系パスをテスト
  • null入力ハンドリングテスト
  • エッジケース(空のコレクション、境界値、負のID、空文字列)テスト
  • 例外シナリオを包括的にテスト
  • 外部依存関係(リポジトリ、サービス、Camelエンドポイント)をモック化
  • 80%以上の行カバレッジ、70%以上のブランチカバレッジ目指す

アサーション

  • AssertJ推奨(JUnitアサーション代わりにassertThat使用)
  • 読みやすさのため流暢なAssertJ API使用:assertThat(list).hasSize(3).contains(item)
  • 例外は、JUnit assertThrowsでキャプチャ、AssertJでメッセージ検証
  • 非スロー成功パスはJUnit assertDoesNotThrow使用
  • コレクションにはextracting(), filteredOn(), containsExactly()使用

統合テスト

  • 統合テスト用に@QuarkusTest使用
  • Quarkusテストの依存関係モック化に@InjectMock使用
  • APIテストに REST Assured優先使用
  • テスト固有設定に@TestProfile使用

イベント駆動テスト

  • AdviceWithMockEndpointでCamelルートテスト
  • 必要に応じて@CamelQuarkusTest注釈使用(スタンドアロンCamelテスト)
  • メッセージ内容、ヘッダー、ルーティングロジック検証
  • エラーハンドリングルートを別個にテスト
  • ユニットテストで外部システム(RabbitMQ、S3、データベース)をモック化

Camel ルートテスト

  • メッセージフロー確認にMockEndpoint使用
  • テスト用ルート変更にエンドポイントをモックに置き換えるAdviceWith使用
  • メッセージ変換と整形テスト
  • 例外処理とデッドレターキューテスト

非同期操作テスト

  • CompletableFutureの成功・失敗シナリオテスト
  • 非同期完了待機に.join()使用
  • CompletableFutureから例外プロパゲーション検証
  • 非同期操作へのLogContextプロパゲーション検証

パフォーマンス

  • テストを高速で分離した状態に保つ
  • 継続モードでテスト実行:mvn quarkus:test
  • 入力バリエーション用に パラメータ化テスト(@ParameterizedTest)使用
  • 再利用可能なテストデータビルダーまたはファクトリメソッド構築

Quarkus固有

  • 最新LTSバージョン(Quarkus 3.x)に留める
  • 定期的にネイティブコンパイル互換性テスト
  • 異なるシナリオでQuarkusテストプロファイル活用
  • ローカルテストにQuarkus dev サービス活用
  • @MockBean代わりに@InjectMock(Quarkus固有)使用

検証ベストプラクティス

  • モック化された依存関係の相互作用は常に検証
  • エラーシナリオでメソッドが呼ばれていないことを確認するにverify(mock, never())使用
  • 複雑な引数マッチングにargThat()使用
  • 呼び出し順序が重要な場合InOrder(Mockitoから)で検証

Alternatives

Compare before choosing