testing-pyramid について
testing-pyramidは、Springアプリケーションのテストを構成する開発者向けのスキルです。単体、スライス、統合テストの分け方を示し、セキュリティ、Testcontainers、命名、よくある問題について例と指針を提供します。
主な機能
- 01単体・スライス・統合テストの構成
- 02Spring MVCとJPAのスライス例
- 03Testcontainersのデータベース設定
- 04テストの命名規則
- 05セキュリティとフレームワークの注意点
活用シーン
- 01Springアプリの各層に適したテストを選択
- 02コンテナ化したデータベースでリポジトリをテスト
- 03セキュリティ適用下のコントローラーテストを作成
- 04よくあるSpringテスト設定ミスを回避
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ rrezartprebreza/ spring-boot-skills --skill testing-pyramidインストール後、Claude Code で/testing-pyramidを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlexamples/good-controller-test.javabad-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()のパターンを使用してください
コメント
Markdown 対応