mcpField
T
testing-pyramid

testing-pyramid

github.com/rrezartprebrezaテスト・品質328130 回インストール6 日前に更新
リポジトリ

testing-pyramid について

testing-pyramidは、Springアプリケーションのテストを構成する開発者向けのスキルです。単体、スライス、統合テストの分け方を示し、セキュリティ、Testcontainers、命名、よくある問題について例と指針を提供します。

主な機能

  1. 01単体・スライス・統合テストの構成
  2. 02Spring MVCとJPAのスライス例
  3. 03Testcontainersのデータベース設定
  4. 04テストの命名規則
  5. 05セキュリティとフレームワークの注意点

活用シーン

  1. 01Springアプリの各層に適したテストを選択
  2. 02コンテナ化したデータベースでリポジトリをテスト
  3. 03セキュリティ適用下のコントローラーテストを作成
  4. 04よくあるSpringテスト設定ミスを回避

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill testing-pyramid

インストール後、Claude Code で/testing-pyramidを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
examples/
good-controller-test.java
bad-controller-test.java

詳細

テストピラミッド

構成

Unit Tests         — fast, no Spring context, mock dependencies    (70%)
Slice Tests        — partial Spring context (@WebMvcTest, @DataJpaTest) (20%)
Integration Tests  — full context + real DB via Testcontainers     (10%)

ユニットテスト — サービス

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock private OrderRepository orderRepository;
    @Mock private InventoryService inventoryService;

    @InjectMocks private OrderService orderService;

    @Test
    void createOrder_whenItemsAvailable_shouldSaveAndReturnOrder() {
        // Given
        var request = new CreateOrderRequest("[email protected]", List.of(new OrderItemRequest(UUID.randomUUID(), 2)));
        var savedOrder = Order.create("[email protected]");
        when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);
        doNothing().when(inventoryService).reserve(any());

        // When
        Order result = orderService.createOrder(request);

        // Then
        assertThat(result).isNotNull();
        assertThat(result.getCustomerEmail()).isEqualTo("[email protected]");
        verify(inventoryService).reserve(request.items());
        verify(orderRepository).save(any(Order.class));
    }

    @Test
    void createOrder_whenInventoryUnavailable_shouldThrowException() {
        // Given
        var request = new CreateOrderRequest("[email protected]", List.of());
        doThrow(new InsufficientInventoryException("Out of stock"))
            .when(inventoryService).reserve(any());

        // When / Then
        assertThatThrownBy(() -> orderService.createOrder(request))
            .isInstanceOf(InsufficientInventoryException.class)
            .hasMessage("Out of stock");
    }
}

スライステスト — コントローラー(@WebMvcTest)

// POSTs need .with(csrf()): @WebMvcTest applies Boot's default security (CSRF on);
// @WithMockUser only authenticates. Add @Import(SecurityConfig.class) to test your real rules.
// import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired MockMvc mockMvc;
    @Autowired ObjectMapper objectMapper;
    @MockitoBean OrderService orderService; // Spring Boot 3.4+: @MockBean is deprecated

    @Test
    @WithMockUser(roles = "USER")
    void createOrder_withValidRequest_shouldReturn201() throws Exception {
        // Given
        var request = new CreateOrderRequest("[email protected]",
            List.of(new OrderItemRequest(UUID.randomUUID(), 1))); // items are @NotEmpty
        var order = Order.create("[email protected]");
        when(orderService.createOrder(any())).thenReturn(order);

        // When / Then
        mockMvc.perform(post("/api/v1/orders").with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.success").value(true))
            .andExpect(jsonPath("$.data.customerEmail").value("[email protected]"));
    }

    @Test
    @WithMockUser
    void createOrder_withInvalidRequest_shouldReturn400() throws Exception {
        mockMvc.perform(post("/api/v1/orders").with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content("{}")) // missing required fields
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.success").value(false))
            .andExpect(jsonPath("$.error.code").value("VALIDATION_FAILED"));
    }
}

スライステスト — リポジトリー(@DataJpaTest)

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // use real DB (Testcontainers)
@Import(TestcontainersConfig.class)
class OrderRepositoryTest {

    @Autowired OrderRepository orderRepository;

    @Test
    void findByStatus_shouldReturnMatchingOrders() {
        // Given
        var order1 = orderRepository.save(Order.create("[email protected]"));
        var order2 = orderRepository.save(Order.create("[email protected]"));
        order2.ship(); // change status
        orderRepository.save(order2);

        // When
        List<Order> pending = orderRepository.findByStatus(OrderStatus.PENDING, Pageable.unpaged()).getContent();

        // Then
        assertThat(pending).hasSize(1);
        assertThat(pending.get(0).getCustomerEmail()).isEqualTo("[email protected]");
    }
}

統合テスト — Testcontainers

このアプリケーションレベルの例では、関連するスキルにあるファーストパーティのJWT設定と注文APIを前提としています。アプリケーションのスキーマに合わせて、src/test/resources/fixtures/order-user-and-product.sql を用意してください。有効なUSERである [email protected] と、IDが 00000000-0000-0000-0000-000000000001 の在庫あり商品を含めます。このSQLは破棄可能なテストデータベースに対してのみ実行してください。フィクスチャとレスポンスDTOはアプリケーションに合わせて調整してください。

トークンに署名する前に、実際に永続化されたユーザーを読み込んでください。@WithMockUser は、ランダムポートのサーバーにHTTPで送信されたリクエストを認証しません。OAuth2 リソースサーバーの場合は、このアプリケーションのJWTサービスを新たに導入するのではなく、テスト用の発行者を使用してください。

// Shared config — reuse container across tests
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfig {

    @Bean
    @ServiceConnection
    PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>("postgres:16-alpine");
    }
}

// Full integration test
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(TestcontainersConfig.class)
class OrderIntegrationTest {

    @Autowired TestRestTemplate restTemplate;
    @Autowired OrderRepository orderRepository;
    @Autowired JwtService jwtService;
    @Autowired UserDetailsService userDetailsService;

    private static final UUID PRODUCT_ID = UUID.fromString("00000000-0000-0000-0000-000000000001");
    private static final ParameterizedTypeReference<ApiResponse<OrderResponse>> RESPONSE_TYPE =
        new ParameterizedTypeReference<>() {};

    @Test
    @Sql("/fixtures/order-user-and-product.sql")
    void createAndRetrieveOrder_endToEnd() {
        var user = userDetailsService.loadUserByUsername("[email protected]");
        String token = jwtService.generateAccessToken(user);
        var createRequest = new CreateOrderRequest("[email protected]",
            List.of(new CreateOrderRequest.OrderItemRequest(PRODUCT_ID, 1)));
        var headers = new HttpHeaders();
        headers.setBearerAuth(token);
        headers.setContentType(MediaType.APPLICATION_JSON);
        var createResponse = restTemplate.exchange("/api/v1/orders", HttpMethod.POST,
            new HttpEntity<>(createRequest, headers), RESPONSE_TYPE);
        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        var created = createResponse.getBody();
        assertThat(created).isNotNull();
        assertThat(created.success()).isTrue();
        assertThat(created.data()).isNotNull();
        UUID id = created.data().id();
        assertThat(id).isNotNull();
        assertThat(orderRepository.findById(id)).isPresent();

        var getResponse = restTemplate.exchange("/api/v1/orders/{id}", HttpMethod.GET,
            new HttpEntity<>(headers), RESPONSE_TYPE, id);
        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        var retrieved = getResponse.getBody();
        assertThat(retrieved).isNotNull();
        assertThat(retrieved.success()).isTrue();
        assertThat(retrieved.data()).isNotNull();
        assertThat(retrieved.data().id()).isEqualTo(id);
        assertThat(retrieved.data().status()).isEqualTo(created.data().status());
        assertThat(retrieved.data().items()).hasSize(1);
        assertThat(retrieved.data().items().get(0).productId()).isEqualTo(PRODUCT_ID);
        assertThat(retrieved.data().items().get(0).quantity()).isEqualTo(1);
    }
}

命名規則

// Method name: methodName_condition_expectedBehavior
createOrder_whenItemsAvailable_shouldSaveOrder()
findById_whenOrderNotFound_shouldThrowNotFoundException()
login_withInvalidCredentials_shouldReturn401()

Testcontainers の依存関係

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>

注意点

  • エージェントはあらゆるテストに @SpringBootTest を使用します。高速化のため、スライスを使用してください
  • エージェントは @DataJpaTest に H2 のインメモリDBを使用します。正確性のため、Testcontainersを使用してください
  • エージェントは @MockBean を使用します。Spring Boot 3.4以降では非推奨です。@MockitoBean を使用してください(スパイには @MockitoSpyBean を使用します)
  • エージェントは @Mock の代わりに Mockito.mock() を使用します。@ExtendWith(MockitoExtension.class) とアノテーションを使用してください
  • エージェントはコントローラーテストで @WithMockUser を忘れます。セキュリティフィルターによって、すべてのリクエストが401でブロックされます
  • エージェントは .with(csrf()) を指定せずに @WebMvcTest でPOSTします。Bootのデフォルトのセキュリティ設定ではCSRFが有効なため、@WithMockUser を指定していても、書き込みリクエストはすべて403を返します
  • エージェントはJUnitの assertEquals を使用します。AssertJ(assertThat(...).isEqualTo(...))を使用してください
  • エージェントはテストを test_createOrder() と命名します。createOrder_condition_expected() のパターンを使用してください

コメント