affaan-m/ECC

quarkus-tdd

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

89Collecting
See how to use itView GitHub source
npx skills add https://github.com/affaan-m/ECC --skill "docs/ja-JP/skills/quarkus-tdd"

Quick start

Start using it in three steps

Install it or open the source, trigger it with a clear task, then follow the source workflow.

1

Install the Skill

npx skills add https://github.com/affaan-m/ECC --skill "docs/ja-JP/skills/quarkus-tdd"
2

Describe the task

Use quarkus-tdd to help me with: [describe your task]. Before you begin, tell me what input you need, the steps you will follow, and the expected output.

3

Follow the workflow

4 key workflow steps, examples, and cautions are distilled below.

Continue to the workflow

Direct answers

Answers to review before you install

What is quarkus-tdd?

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

Who should use quarkus-tdd?

It is relevant to workflows involving Testing, Engineering, Operations.

How do you install quarkus-tdd?

SkillSignal detected this source-specific command: npx skills add https://github.com/affaan-m/ECC --skill "docs/ja-JP/skills/quarkus-tdd". Inspect the repository and command before running it.

Which Agent platforms does it support?

The upstream source does not declare a dedicated Agent platform.

What permissions or risks should you review?

No obvious permission action was detected by the static rules. This is not proof that the Skill is safe.

What are the current evidence limits?

This page combines upstream documentation with deterministic repository, quality, and static-risk signals. It is not described as a manual test or security review.

SkillSignal brief

Decide whether it fits your work first

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

Useful in these contexts

Not yet included in a workflow collection

Core capabilities

TestingEngineeringOperations

Distilled from the source

Understand this Skill in one minute

About 2 min · 12 sections

When it is worth using

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

  2. バグ修正またはリファクタリング

  3. データアクセスロジック、セキュリティルール、またはリアクティブストリーム追加

  4. Apache Camelルートとイベントハンドラーテスト

Core workflow

  1. 1

    テストを先に書く(失敗するはず)

  2. 2

    最小限のコードで合格実装

  3. 3

    テストが緑の状態でリファクタリング

  4. 4

    JaCoCoでカバレッジ実装(80%以上を目標)

Repository stars
234,327
Repository forks
35,711
Quality
89/100
Source repository last pushed

Quality breakdown

Based on traceable docs and repository signals; stars are not treated as quality.

89/100
Documentation29/30
Specificity23/25
Maintenance20/20
Trust signals17/25

Compare before choosing

Related Agent Skills and source variants

These links are selected from shared tasks, functions, stacks, platforms, and same-name variants. Compare the source owner, documentation, permissions, and maintenance signals.

View original Skill.mdThis page is parsed directly from the repository SKILL.md without editorial rewriting. Collected: Jul 28, 2026 · about 2 min

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から)で検証
Source repo
affaan-m/ECC
Skill path
docs/ja-JP/skills/quarkus-tdd/SKILL.md
Commit SHA
4e973d3eaf92
Repository license
MIT
Data collected