Skip to content
cccskills
FreePublished on GitHub

generate-test-specs

テスト仕様生成エージェント - AIエージェントがテストコードを実装可能なBDDシナリオ・ユニットテスト・統合テスト仕様を生成。Gherkin形式のシナリオとJUnit/Jest形式のテストケースを策定。/generate-test-specs [対象パス] で呼び出し。

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md35.5 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Test Specification Generator Agent

AIエージェントがテストコードを実装可能なレベルのテスト仕様を生成するエージェントです。

概要

このエージェントは、実装仕様から以下のテスト仕様を生成します:

  1. BDDシナリオ (Gherkin) - ビジネス要件ベースのシナリオ
  2. ユニットテスト仕様 - クラス・メソッド単位のテスト
  3. 統合テスト仕様 - サービス間連携のテスト
  4. エッジケース・エラーケース - 境界値・異常系テスト
  5. パフォーマンステスト仕様 - 負荷・応答時間テスト

前提条件

以下のファイルが存在すること:

必須:

  • reports/06_implementation/domain-services-spec.md ← /design-implementation
  • reports/06_implementation/repository-interfaces-spec.md ← /design-implementation

推奨:

  • reports/06_implementation/value-objects-spec.md ← /design-implementation
  • reports/06_implementation/exception-mapping.md ← /design-implementation
  • reports/06_implementation/saga-orchestration-spec.md ← /design-implementation
  • reports/03_design/api-specifications/ ← /design-api

出力先

結果は reports/07_test-specs/ に出力します。 重要: 各ステップ完了時に即座にファイルを出力してください。

reports/07_test-specs/
├── bdd-scenarios/              # Step 2完了時
│   ├── order-scenarios.feature
│   └── [domain]-scenarios.feature
├── unit-test-specs.md          # Step 3完了時
├── integration-test-specs.md   # Step 4完了時
├── edge-case-specs.md          # Step 5完了時
├── performance-test-specs.md   # Step 6完了時
└── test-data-requirements.md   # Step 7完了時

実行プロンプト

あなたはテスト設計の専門家です。AIエージェントがテストコードを実装可能な詳細仕様を生成してください。

Step 0: 前提条件の検証

重要: 実行前に必ず前提条件を確認してください。

必須ファイルの確認:
├── reports/06_implementation/domain-services-spec.md    [必須]
└── reports/06_implementation/repository-interfaces-spec.md [必須]

推奨ファイルの確認:
├── reports/06_implementation/value-objects-spec.md      [推奨]
├── reports/06_implementation/exception-mapping.md       [推奨]
├── reports/06_implementation/saga-orchestration-spec.md [推奨]
└── reports/03_design/api-specifications/                [推奨]

エラーハンドリング:

  • 必須ファイルが存在しない場合 → /design-implementation を先に実行するよう案内
  • 推奨ファイルが存在しない場合 → 警告を表示して続行

Step 1: 実装仕様の読み込み

前提ファイルから以下の情報を抽出:

  1. ドメインサービス - メソッド、バリデーション、例外
  2. リポジトリ - メソッド、クエリ
  3. 値オブジェクト - バリデーションルール
  4. 例外 - 発生条件、HTTPマッピング
  5. Saga - ステップ、補償トランザクション

Step 2: BDDシナリオ (Gherkin) の生成

出力: reports/07_test-specs/bdd-scenarios/[domain]-scenarios.feature

各ドメインのビジネスシナリオをGherkin形式で出力:

# order-scenarios.feature

@order @acceptance
Feature: 注文管理
  ユーザーとして
  商品を注文したい
  購入手続きを完了するために

  Background:
    Given 以下の商品が存在する:
      | productId | name      | price | stock |
      | PROD-001  | 商品A     | 1000  | 100   |
      | PROD-002  | 商品B     | 2000  | 50    |
    And 顧客 "customer-123" が認証済みである

  # ==============================
  # 正常系シナリオ
  # ==============================

  @happy-path
  Scenario: 単一商品の注文を作成できる
    Given 商品 "PROD-001" の在庫が 100 個ある
    When 以下の注文を作成する:
      | productId | quantity |
      | PROD-001  | 2        |
    Then 注文が作成される
    And 注文ステータスは "PENDING" である
    And 合計金額は 2000 円である
    And 在庫が 98 個に減少する
    And "OrderCreated" イベントが発行される

  @happy-path
  Scenario: 複数商品の注文を作成できる
    Given 商品 "PROD-001" の在庫が 100 個ある
    And 商品 "PROD-002" の在庫が 50 個ある
    When 以下の注文を作成する:
      | productId | quantity |
      | PROD-001  | 2        |
      | PROD-002  | 3        |
    Then 注文が作成される
    And 合計金額は 8000 円である

  @happy-path
  Scenario: 注文をキャンセルできる
    Given 注文 "order-123" が "PENDING" ステータスで存在する
    When 注文 "order-123" をキャンセルする
    Then 注文ステータスは "CANCELLED" に変更される
    And 予約在庫が解放される
    And "OrderCancelled" イベントが発行される

  # ==============================
  # 異常系シナリオ
  # ==============================

  @error-case
  Scenario: 在庫不足の場合は注文作成に失敗する
    Given 商品 "PROD-001" の在庫が 5 個ある
    When 以下の注文を作成する:
      | productId | quantity |
      | PROD-001  | 10       |
    Then エラーコード "INSUFFICIENT_INVENTORY" が返される
    And 注文は作成されない
    And 在庫は変更されない

  @error-case
  Scenario: 存在しない商品の注文は失敗する
    When 以下の注文を作成する:
      | productId     | quantity |
      | NON-EXISTENT  | 1        |
    Then エラーコード "PRODUCT_NOT_FOUND" が返される

  @error-case
  Scenario: 数量0の注文は失敗する
    When 以下の注文を作成する:
      | productId | quantity |
      | PROD-001  | 0        |
    Then エラーコード "INVALID_ORDER" が返される
    And エラーメッセージに "数量は1以上" が含まれる

  @error-case
  Scenario: 既にキャンセル済みの注文は再キャンセルできない
    Given 注文 "order-123" が "CANCELLED" ステータスで存在する
    When 注文 "order-123" をキャンセルする
    Then エラーコード "ORDER_ALREADY_CANCELLED" が返される

  # ==============================
  # 境界値シナリオ
  # ==============================

  @boundary
  Scenario: 最大数量での注文
    Given 商品 "PROD-001" の在庫が 10000 個ある
    When 以下の注文を作成する:
      | productId | quantity |
      | PROD-001  | 9999     |
    Then 注文が作成される

  @boundary
  Scenario: 最大数量超過の注文は失敗
    When 以下の注文を作成する:
      | productId | quantity |
      | PROD-001  | 10000    |
    Then エラーコード "INVALID_ORDER" が返される
    And エラーメッセージに "数量は9999以下" が含まれる

  @boundary
  Scenario: 最大商品数での注文
    Given 100種類の商品が存在する
    When 100種類の商品を各1個ずつ注文する
    Then 注文が作成される

  @boundary
  Scenario: 商品数超過の注文は失敗
    Given 101種類の商品が存在する
    When 101種類の商品を各1個ずつ注文する
    Then エラーコード "INVALID_ORDER" が返される
    And エラーメッセージに "100件以下" が含まれる

  # ==============================
  # 並行処理シナリオ
  # ==============================

  @concurrency
  Scenario: 同時注文による在庫競合
    Given 商品 "PROD-001" の在庫が 10 個ある
    When 以下の注文が同時に実行される:
      | customerId    | productId | quantity |
      | customer-001  | PROD-001  | 8        |
      | customer-002  | PROD-001  | 8        |
    Then 1件の注文が成功する
    And 1件の注文は "INSUFFICIENT_INVENTORY" で失敗する
    And 在庫の整合性が保たれる

  @concurrency
  Scenario: 同一注文の同時更新
    Given 注文 "order-123" が "PENDING" ステータスで存在する
    When 以下の操作が同時に実行される:
      | operation | orderId   |
      | cancel    | order-123 |
      | confirm   | order-123 |
    Then 1件の操作が成功する
    And 1件の操作は "CONCURRENT_MODIFICATION" で失敗する

Step 3: ユニットテスト仕様の生成

出力: reports/07_test-specs/unit-test-specs.md

# ユニットテスト仕様

## Order Service

### OrderApplicationService

#### createOrder

| テストケース | Given | When | Then | Mock |
|------------|-------|------|------|------|
| 正常系_単一商品 | 顧客存在, 在庫十分 | createOrder(1商品) | 注文作成, イベント発行 | CustomerRepo, InventoryService |
| 正常系_複数商品 | 顧客存在, 全商品在庫十分 | createOrder(3商品) | 注文作成, 合計金額正確 | - |
| 異常系_顧客不在 | 顧客不存在 | createOrder | CustomerNotFoundException | CustomerRepo returns empty |
| 異常系_在庫不足 | 在庫 < 要求数量 | createOrder | InsufficientInventoryException | InventoryService throws |
| 異常系_空の商品リスト | items = [] | createOrder | InvalidOrderException | - |
| 異常系_数量0 | quantity = 0 | createOrder | InvalidOrderException | - |
| 異常系_数量マイナス | quantity = -1 | createOrder | InvalidOrderException | - |

```java
// テストクラステンプレート

@ExtendWith(MockitoExtension.class)
class OrderApplicationServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private CustomerRepository customerRepository;

    @Mock
    private InventoryService inventoryService;

    @Mock
    private EventPublisher eventPublisher;

    @InjectMocks
    private OrderApplicationService sut;

    @Nested
    @DisplayName("createOrder")
    class CreateOrderTest {

        @Test
        @DisplayName("正常系: 単一商品の注文を作成できる")
        void shouldCreateOrderWithSingleItem() {
            // Given
            CustomerId customerId = CustomerId.of("customer-123");
            ProductId productId = ProductId.of("product-456");
            CreateOrderCommand command = CreateOrderCommand.builder()
                .customerId(customerId)
                .items(List.of(
                    OrderItemCommand.of(productId, Quantity.of(2))
                ))
                .shippingAddress(testAddress())
                .build();

            when(customerRepository.findById(customerId))
                .thenReturn(Optional.of(testCustomer()));
            when(inventoryService.reserveInventory(any()))
                .thenReturn(ReservationResult.success("res-123"));

            // When
            Order result = sut.createOrder(command);

            // Then
            assertThat(result).isNotNull();
            assertThat(result.getStatus()).isEqualTo(OrderStatus.PENDING);
            assertThat(result.getItems()).hasSize(1);

            verify(orderRepository).save(any(Order.class));
            verify(eventPublisher).publish(any(OrderCreatedEvent.class));
        }

        @Test
        @DisplayName("異常系: 顧客が存在しない場合はCustomerNotFoundExceptionをスロー")
        void shouldThrowWhenCustomerNotFound() {
            // Given
            CustomerId customerId = CustomerId.of("non-existent");
            CreateOrderCommand command = CreateOrderCommand.builder()
                .customerId(customerId)
                .items(List.of(testOrderItem()))
                .shippingAddress(testAddress())
                .build();

            when(customerRepository.findById(customerId))
                .thenReturn(Optional.empty());

            // When & Then
            assertThatThrownBy(() -> sut.createOrder(command))
                .isInstanceOf(CustomerNotFoundException.class)
                .hasMessageContaining("non-existent");

            verify(orderRepository, never()).save(any());
            verify(eventPublisher, never()).publish(any());
        }

        @Test
        @DisplayName("異常系: 在庫不足の場合はInsufficientInventoryExceptionをスロー")
        void shouldThrowWhenInsufficientInventory() {
            // Given
            when(customerRepository.findById(any()))
                .thenReturn(Optional.of(testCustomer()));
            when(inventoryService.reserveInventory(any()))
                .thenThrow(new InsufficientInventoryException(
                    ProductId.of("prod-123"),
                    Quantity.of(10),
                    Quantity.of(3)
                ));

            // When & Then
            assertThatThrownBy(() -> sut.createOrder(testCommand()))
                .isInstanceOf(InsufficientInventoryException.class);

            verify(orderRepository, never()).save(any());
        }
    }
}

Order (集約ルート)

テストケースGivenWhenThen
作成_正常有効なパラメータOrder.create()Orderインスタンス, ステータスPENDING
キャンセル_PENDINGstatus=PENDINGcancel()status=CANCELLED, イベント記録
キャンセル_CONFIRMEDstatus=CONFIRMEDcancel()status=CANCELLED
キャンセル_SHIPPEDstatus=SHIPPEDcancel()InvalidStateTransitionException
キャンセル_既にCANCELLEDstatus=CANCELLEDcancel()OrderAlreadyCancelledException
確定_PENDINGstatus=PENDINGconfirm()status=CONFIRMED
確定_CANCELLEDstatus=CANCELLEDconfirm()InvalidStateTransitionException
@DisplayName("Order 集約ルートのテスト")
class OrderTest {

    @Nested
    @DisplayName("cancel")
    class CancelTest {

        @Test
        @DisplayName("PENDINGステータスの注文をキャンセルできる")
        void canCancelPendingOrder() {
            // Given
            Order order = createOrderWithStatus(OrderStatus.PENDING);

            // When
            order.cancel("顧客都合");

            // Then
            assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED);
            assertThat(order.getDomainEvents())
                .hasSize(1)
                .first()
                .isInstanceOf(OrderCancelledEvent.class);
        }

        @Test
        @DisplayName("SHIPPEDステータスの注文はキャンセルできない")
        void cannotCancelShippedOrder() {
            // Given
            Order order = createOrderWithStatus(OrderStatus.SHIPPED);

            // When & Then
            assertThatThrownBy(() -> order.cancel("顧客都合"))
                .isInstanceOf(InvalidStateTransitionException.class)
                .hasMessageContaining("SHIPPED")
                .hasMessageContaining("CANCELLED");
        }
    }
}

値オブジェクトテスト

Money

テストケース入力期待結果
生成_正常1000, JPYMoneyインスタンス
生成_0円0, JPYMoneyインスタンス
生成_負の金額-100, JPYIllegalArgumentException
生成_上限超過10^12, JPYIllegalArgumentException
加算_同一通貨1000 + 5001500
加算_異なる通貨1000 JPY + 10 USDCurrencyMismatchException
減算_結果正1000 - 300700
減算_結果負100 - 200NegativeMoneyException
乗算1000 * 1.11100 (HALF_UP)
等価性同額同通貨true
@DisplayName("Money 値オブジェクトのテスト")
class MoneyTest {

    @Test
    @DisplayName("正の金額でMoneyを生成できる")
    void canCreateWithPositiveAmount() {
        Money money = Money.of(1000);
        assertThat(money.amount()).isEqualTo(1000);
        assertThat(money.currency()).isEqualTo(Currency.JPY);
    }

    @Test
    @DisplayName("負の金額ではIllegalArgumentExceptionをスロー")
    void throwsForNegativeAmount() {
        assertThatThrownBy(() -> Money.of(-100))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("金額は0以上");
    }

    @ParameterizedTest
    @CsvSource({
        "1000, 500, 1500",
        "0, 100, 100",
        "999999999999, 0, 999999999999"
    })
    @DisplayName("同一通貨の加算")
    void addSameCurrency(long a, long b, long expected) {
        Money result = Money.of(a).add(Money.of(b));
        assertThat(result.amount()).isEqualTo(expected);
    }

    @Test
    @DisplayName("異なる通貨の加算はCurrencyMismatchExceptionをスロー")
    void throwsForDifferentCurrency() {
        Money jpy = Money.of(1000, Currency.JPY);
        Money usd = Money.of(10, Currency.USD);

        assertThatThrownBy(() -> jpy.add(usd))
            .isInstanceOf(CurrencyMismatchException.class);
    }
}

### Step 4: 統合テスト仕様の生成

**出力**: `reports/07_test-specs/integration-test-specs.md`

```markdown
# 統合テスト仕様

## API統合テスト

### Order API

| エンドポイント | メソッド | シナリオ | 期待ステータス | 検証項目 |
|--------------|--------|---------|--------------|---------|
| /api/v1/orders | POST | 正常な注文作成 | 201 | Location, Body |
| /api/v1/orders | POST | 認証なし | 401 | エラーレスポンス |
| /api/v1/orders | POST | 無効なリクエスト | 400 | バリデーションエラー |
| /api/v1/orders | POST | 在庫不足 | 422 | ビジネスエラー |
| /api/v1/orders/{id} | GET | 存在する注文 | 200 | 注文詳細 |
| /api/v1/orders/{id} | GET | 存在しない注文 | 404 | NOT_FOUND |
| /api/v1/orders/{id} | GET | 他人の注文 | 403 | FORBIDDEN |
| /api/v1/orders | GET | 一覧取得 | 200 | ページング, フィルタ |

```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderApiIntegrationTest {

    @Container
    static ScalarDbContainer scalarDb = new ScalarDbContainer();

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private OrderRepository orderRepository;

    @BeforeEach
    void setUp() {
        // テストデータ初期化
    }

    @Nested
    @DisplayName("POST /api/v1/orders")
    class CreateOrderTest {

        @Test
        @DisplayName("正常: 注文を作成できる")
        void shouldCreateOrder() {
            // Given
            String token = getAuthToken("customer-123");
            CreateOrderRequest request = CreateOrderRequest.builder()
                .items(List.of(new OrderItemRequest("prod-001", 2)))
                .shippingAddress(testAddress())
                .build();

            // When
            ResponseEntity<OrderResponse> response = restTemplate.exchange(
                "/api/v1/orders",
                HttpMethod.POST,
                new HttpEntity<>(request, authHeaders(token)),
                OrderResponse.class
            );

            // Then
            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
            assertThat(response.getHeaders().getLocation()).isNotNull();

            OrderResponse body = response.getBody();
            assertThat(body.id()).isNotNull();
            assertThat(body.status()).isEqualTo("PENDING");
            assertThat(body.items()).hasSize(1);

            // DB検証
            Optional<Order> saved = orderRepository.findById(OrderId.of(body.id()));
            assertThat(saved).isPresent();
        }

        @Test
        @DisplayName("異常: 認証なしで401")
        void shouldReturn401WhenNoAuth() {
            CreateOrderRequest request = testRequest();

            ResponseEntity<ErrorResponse> response = restTemplate.exchange(
                "/api/v1/orders",
                HttpMethod.POST,
                new HttpEntity<>(request),
                ErrorResponse.class
            );

            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
        }

        @Test
        @DisplayName("異常: バリデーションエラーで400")
        void shouldReturn400ForInvalidRequest() {
            String token = getAuthToken("customer-123");
            CreateOrderRequest request = CreateOrderRequest.builder()
                .items(List.of())  // 空のitems
                .build();

            ResponseEntity<ErrorResponse> response = restTemplate.exchange(
                "/api/v1/orders",
                HttpMethod.POST,
                new HttpEntity<>(request, authHeaders(token)),
                ErrorResponse.class
            );

            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
            assertThat(response.getBody().code()).isEqualTo("VALIDATION_ERROR");
        }
    }
}

サービス間統合テスト

Order-Inventory連携

シナリオ手順期待結果検証項目
正常な在庫予約注文作成 → 在庫確認在庫減少, 予約作成在庫数, 予約レコード
注文キャンセル時の在庫解放注文作成 → キャンセル在庫復元在庫数が元に戻る
在庫サービス障害時のリトライ注文作成 (在庫API一時障害)リトライ後成功リトライ回数, 最終結果
在庫サービス長期障害時のフォールバック注文作成 (在庫API長期障害)適切なエラーエラーメッセージ, 注文未作成
@SpringBootTest
@Testcontainers
class OrderInventoryIntegrationTest {

    @Container
    static KafkaContainer kafka = new KafkaContainer();

    @MockBean
    private InventoryServiceClient inventoryClient;

    @Autowired
    private OrderApplicationService orderService;

    @Test
    @DisplayName("注文作成時に在庫が予約される")
    void shouldReserveInventoryOnOrderCreation() {
        // Given
        when(inventoryClient.reserveInventory(any()))
            .thenReturn(ReservationResult.success("res-123"));

        // When
        Order order = orderService.createOrder(testCommand());

        // Then
        verify(inventoryClient).reserveInventory(argThat(req ->
            req.getItems().size() == 1 &&
            req.getItems().get(0).getQuantity() == 2
        ));
    }

    @Test
    @DisplayName("注文キャンセル時に在庫予約が解放される")
    void shouldReleaseInventoryOnCancel() {
        // Given
        Order order = createTestOrder();

        // When
        orderService.cancelOrder(order.getId(), "顧客都合");

        // Then
        verify(inventoryClient).releaseInventory("res-123");
    }

    @Test
    @DisplayName("在庫サービス障害時にリトライする")
    void shouldRetryOnInventoryServiceFailure() {
        // Given
        when(inventoryClient.reserveInventory(any()))
            .thenThrow(new ServiceUnavailableException())
            .thenThrow(new ServiceUnavailableException())
            .thenReturn(ReservationResult.success("res-123"));

        // When
        Order order = orderService.createOrder(testCommand());

        // Then
        verify(inventoryClient, times(3)).reserveInventory(any());
        assertThat(order).isNotNull();
    }
}

Sagaテスト

CreateOrderSaga

シナリオステップ期待動作補償確認
全ステップ成功注文→在庫→決済→確定注文CONFIRMED-
在庫予約失敗注文→在庫(失敗)注文CANCELLED注文キャンセル
決済失敗注文→在庫→決済(失敗)注文CANCELLED在庫解放, 注文キャンセル
確定失敗注文→在庫→決済→確定(失敗)注文CANCELLED返金, 在庫解放, 注文キャンセル
@SpringBootTest
class CreateOrderSagaIntegrationTest {

    @Test
    @DisplayName("全ステップ成功時に注文が確定される")
    void shouldConfirmOrderWhenAllStepsSucceed() {
        // Given
        mockInventorySuccess();
        mockPaymentSuccess();

        // When
        SagaResult result = sagaOrchestrator.execute(createOrderSaga());

        // Then
        assertThat(result.isSuccess()).isTrue();
        assertThat(orderRepository.findById(orderId).get().getStatus())
            .isEqualTo(OrderStatus.CONFIRMED);
    }

    @Test
    @DisplayName("決済失敗時に補償トランザクションが実行される")
    void shouldCompensateWhenPaymentFails() {
        // Given
        mockInventorySuccess();
        mockPaymentFailure();

        // When
        SagaResult result = sagaOrchestrator.execute(createOrderSaga());

        // Then
        assertThat(result.isSuccess()).isFalse();

        // 補償確認
        verify(inventoryService).releaseInventory(any());
        assertThat(orderRepository.findById(orderId).get().getStatus())
            .isEqualTo(OrderStatus.CANCELLED);
    }
}

### Step 5: エッジケース・エラーケース仕様の生成

**出力**: `reports/07_test-specs/edge-case-specs.md`

```markdown
# エッジケース・エラーケース仕様

## 境界値テスト

### Money

| ケース | 入力 | 期待結果 |
|-------|-----|---------|
| 最小値 | 0 | 成功 |
| 最小値-1 | -1 | IllegalArgumentException |
| 最大値 | 999,999,999,999 | 成功 |
| 最大値+1 | 1,000,000,000,000 | IllegalArgumentException |
| 加算でオーバーフロー | MAX + 1 | ArithmeticException |

### Quantity

| ケース | 入力 | 期待結果 |
|-------|-----|---------|
| 最小値 | 1 | 成功 |
| 最小値-1 | 0 | IllegalArgumentException |
| 最大値 | 9999 | 成功 |
| 最大値+1 | 10000 | IllegalArgumentException |

### Order Items

| ケース | 入力 | 期待結果 |
|-------|-----|---------|
| 最小商品数 | 1件 | 成功 |
| 最小商品数-1 | 0件 | InvalidOrderException |
| 最大商品数 | 100件 | 成功 |
| 最大商品数+1 | 101件 | InvalidOrderException |

## Null/Empty処理

| フィールド | null | 空文字 | 空白のみ | 期待結果 |
|-----------|------|-------|---------|---------|
| customerId | ❌ | ❌ | ❌ | NullPointerException / IllegalArgumentException |
| items | ❌ | - | - | InvalidOrderException |
| shippingAddress | ❌ | - | - | NullPointerException |
| postalCode | ❌ | ❌ | ❌ | ValidationException |
| notes (任意) | ✅ | ✅ | ✅ | 許可 |

## 同時実行・競合

| シナリオ | 条件 | 期待結果 |
|---------|-----|---------|
| 楽観ロック競合 | 同一注文を2トランザクションで更新 | 1件成功, 1件OptimisticLockException |
| 在庫競合 | 残り10個を2顧客が8個ずつ注文 | 1件成功, 1件InsufficientInventoryException |
| デッドロック回避 | Order→Inventory, Inventory→Order同時 | タイムアウトでリトライ |

## タイムアウト・リトライ

| シナリオ | 条件 | 期待結果 |
|---------|-----|---------|
| 在庫サービスタイムアウト | 応答 > 3秒 | リトライ3回後ServiceUnavailableException |
| 決済サービス一時障害 | 1,2回目失敗, 3回目成功 | 最終成功 |
| 決済サービス永続障害 | 全リトライ失敗 | PaymentFailedException + 補償 |

## セキュリティ

| シナリオ | 入力 | 期待結果 |
|---------|-----|---------|
| 他人の注文参照 | /orders/{他人のID} | 403 Forbidden |
| 他人の注文キャンセル | 他人の注文をキャンセル | 403 Forbidden |
| 不正なJWT | 期限切れトークン | 401 Unauthorized |
| スコープ不足 | orders:read のみでPOST | 403 Forbidden |
| SQLインジェクション | productId: "'; DROP TABLE--" | 無害化されエラー |

## データ整合性

| シナリオ | 条件 | 検証 |
|---------|-----|------|
| 注文作成中断 | 在庫予約後にプロセスクラッシュ | 孤立予約の検出・クリーンアップ |
| イベント発行失敗 | Kafkaダウン | 注文ロールバック or Outbox |
| DB接続断 | トランザクション中に切断 | ロールバック確認 |

Step 6: パフォーマンステスト仕様の生成

出力: reports/07_test-specs/performance-test-specs.md

# パフォーマンステスト仕様

## SLA要件

| API | p50 | p95 | p99 | スループット |
|-----|-----|-----|-----|------------|
| POST /orders | 100ms | 300ms | 500ms | 100 req/s |
| GET /orders/{id} | 50ms | 100ms | 200ms | 500 req/s |
| GET /orders | 100ms | 300ms | 500ms | 200 req/s |

## 負荷テストシナリオ

### 通常負荷

```yaml
scenario: normal_load
duration: 10m
users:
  constant: 50
requests:
  - endpoint: POST /orders
    weight: 20%
    think_time: 2s
  - endpoint: GET /orders/{id}
    weight: 50%
    think_time: 1s
  - endpoint: GET /orders
    weight: 30%
    think_time: 3s
assertions:
  - p95 < 300ms
  - error_rate < 0.1%

ピーク負荷

scenario: peak_load
duration: 5m
users:
  ramp_up: 100 over 2m
  hold: 100 for 3m
assertions:
  - p99 < 1s
  - error_rate < 1%

スパイク負荷

scenario: spike_load
duration: 5m
users:
  pattern:
    - 10 for 1m
    - 200 for 30s
    - 10 for 1m
    - 200 for 30s
    - 10 for 2m
assertions:
  - recovery_time < 30s
  - no_5xx_after_recovery

k6スクリプト例

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 },
    { duration: '5m', target: 50 },
    { duration: '2m', target: 100 },
    { duration: '5m', target: 100 },
    { duration: '2m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<300', 'p(99)<500'],
    http_req_failed: ['rate<0.01'],
  },
};

export default function() {
  const token = getAuthToken();

  // 注文作成 (20%)
  if (Math.random() < 0.2) {
    const payload = JSON.stringify({
      items: [{ productId: 'prod-001', quantity: 1 }],
      shippingAddress: testAddress(),
    });

    const res = http.post(
      `${BASE_URL}/api/v1/orders`,
      payload,
      { headers: { 'Authorization': `Bearer ${token}` } }
    );

    check(res, {
      'create order status 201': (r) => r.status === 201,
      'create order time < 500ms': (r) => r.timings.duration < 500,
    });
  }

  // 注文参照 (50%)
  else if (Math.random() < 0.7) {
    const orderId = getRandomOrderId();
    const res = http.get(
      `${BASE_URL}/api/v1/orders/${orderId}`,
      { headers: { 'Authorization': `Bearer ${token}` } }
    );

    check(res, {
      'get order status 200': (r) => r.status === 200,
      'get order time < 200ms': (r) => r.timings.duration < 200,
    });
  }

  // 注文一覧 (30%)
  else {
    const res = http.get(
      `${BASE_URL}/api/v1/orders?pageSize=20`,
      { headers: { 'Authorization': `Bearer ${token}` } }
    );

    check(res, {
      'list orders status 200': (r) => r.status === 200,
      'list orders time < 500ms': (r) => r.timings.duration < 500,
    });
  }

  sleep(1);
}

### Step 7: テストデータ要件の生成

**出力**: `reports/07_test-specs/test-data-requirements.md`

```markdown
# テストデータ要件

## マスターデータ

### 顧客

| ID | 名前 | ステータス | 用途 |
|----|-----|----------|-----|
| customer-001 | テスト顧客A | ACTIVE | 正常系テスト |
| customer-002 | テスト顧客B | ACTIVE | 並行テスト |
| customer-003 | 休止顧客 | SUSPENDED | 異常系テスト |

### 商品

| ID | 名前 | 価格 | 初期在庫 | 用途 |
|----|-----|------|---------|-----|
| prod-001 | 標準商品A | 1000 | 1000 | 正常系 |
| prod-002 | 標準商品B | 2000 | 500 | 正常系 |
| prod-003 | 高額商品 | 999999 | 10 | 境界値 |
| prod-004 | 在庫少商品 | 1000 | 5 | 在庫不足テスト |
| prod-005 | 在庫なし商品 | 1000 | 0 | 在庫なしテスト |

## トランザクションデータ

### 注文

| ID | 顧客 | ステータス | 用途 |
|----|-----|----------|-----|
| order-pending-001 | customer-001 | PENDING | キャンセルテスト |
| order-confirmed-001 | customer-001 | CONFIRMED | ステータス遷移テスト |
| order-shipped-001 | customer-001 | SHIPPED | キャンセル不可テスト |
| order-cancelled-001 | customer-001 | CANCELLED | 再キャンセルテスト |

## データセットアップ

### JUnitセットアップ

```java
@TestConfiguration
public class TestDataConfig {

    @Bean
    public TestDataInitializer testDataInitializer(
        CustomerRepository customerRepository,
        ProductRepository productRepository,
        InventoryRepository inventoryRepository
    ) {
        return new TestDataInitializer(
            customerRepository,
            productRepository,
            inventoryRepository
        );
    }
}

@Component
public class TestDataInitializer {

    @PostConstruct
    public void init() {
        // 顧客データ
        customerRepository.save(Customer.builder()
            .id(CustomerId.of("customer-001"))
            .name("テスト顧客A")
            .status(CustomerStatus.ACTIVE)
            .build());

        // 商品データ
        productRepository.save(Product.builder()
            .id(ProductId.of("prod-001"))
            .name("標準商品A")
            .price(Money.of(1000))
            .build());

        // 在庫データ
        inventoryRepository.save(Inventory.builder()
            .productId(ProductId.of("prod-001"))
            .quantity(Quantity.of(1000))
            .build());
    }
}

SQLセットアップ

-- test-data.sql

-- 顧客
INSERT INTO customers (id, name, status) VALUES
  ('customer-001', 'テスト顧客A', 'ACTIVE'),
  ('customer-002', 'テスト顧客B', 'ACTIVE'),
  ('customer-003', '休止顧客', 'SUSPENDED');

-- 商品
INSERT INTO products (id, name, price) VALUES
  ('prod-001', '標準商品A', 1000),
  ('prod-002', '標準商品B', 2000),
  ('prod-003', '高額商品', 999999),
  ('prod-004', '在庫少商品', 1000),
  ('prod-005', '在庫なし商品', 1000);

-- 在庫
INSERT INTO inventory (product_id, quantity) VALUES
  ('prod-001', 1000),
  ('prod-002', 500),
  ('prod-003', 10),
  ('prod-004', 5),
  ('prod-005', 0);

データクリーンアップ

@AfterEach
void cleanUp() {
    // トランザクションデータのみ削除
    // マスターデータは保持
    orderRepository.deleteAll();
    inventoryReservationRepository.deleteAll();
}

### Step 8: Mermaid図の検証

出力したファイルのMermaid図を検証し、エラーがあれば修正:

```bash
/fix-mermaid ./reports/07_test-specs

出力フォーマット

ファイル内容
bdd-scenarios/*.featureGherkin形式のBDDシナリオ
unit-test-specs.mdユニットテスト仕様とコード例
integration-test-specs.md統合テスト仕様
edge-case-specs.md境界値・エラーケース仕様
performance-test-specs.mdパフォーマンステスト仕様
test-data-requirements.mdテストデータ定義

ツール活用ガイドライン

実装仕様読み込み

# ドメインサービス仕様
Read reports/06_implementation/domain-services-spec.md

# 例外マッピング
Read reports/06_implementation/exception-mapping.md

エラーハンドリング

  • 前提ファイル不足 → /design-implementation の実行を案内
  • 仕様情報不足 → 最小限のテストケースを生成、TODO注記を追加

関連スキル

スキル用途
/design-implementation実装仕様(入力)
/design-apiAPI仕様(入力)

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

アクセス制御分析エージェント - ゼロトラストモデルに基づくアクセス制御の現状分析と移行準備評価。/access-control-analysis [対象パス] で呼び出し。

The English summary is being prepared. Showing the original description.

wfukatsu/architecture-redesign-agent5Updated Feb 12, 2026

システム分析エージェント - ユビキタス言語、アクター、ロール、権限、ドメイン-コード対応表を抽出。/analyze-system [対象パス] で呼び出し。

The English summary is being prepared. Showing the original description.

wfukatsu/architecture-redesign-agent5Updated Feb 12, 2026

GraphDB構築エージェント - ユビキタス言語とコード解析結果からRyuGraphデータベースを構築。/build-graph [対象パス] で呼び出し。

The English summary is being prepared. Showing the original description.

wfukatsu/architecture-redesign-agent5Updated Feb 12, 2026

レポートコンパイルエージェント - 分析結果のMarkdownファイルを統合HTMLレポートに変換。/compile-report [出力パス] で呼び出し。

The English summary is being prepared. Showing the original description.

wfukatsu/architecture-redesign-agent5Updated Feb 12, 2026

ドメインストーリーテリングエージェント - ドメインストーリーの作成、ビジネスプロセスの可視化。/create-domain-story --domain=[ドメイン名] で呼び出し。

The English summary is being prepared. Showing the original description.

wfukatsu/architecture-redesign-agent5Updated Feb 12, 2026

データモデル分析エージェント - 既存のデータモデルを分析し、エンティティ・リレーションシップ・ドメインルールを抽出。/data-model-analysis [対象パス] で呼び出し。

The English summary is being prepared. Showing the original description.

wfukatsu/architecture-redesign-agent5Updated Feb 12, 2026

See all skills by wfukatsu

Report a problem with this skill