关于 testing-pyramid
testing-pyramid 是面向 Spring 应用开发者的测试技能,帮助组织服务、控制器、仓库和集成测试。它介绍单元、切片和集成测试的分层,并提供安全配置、Testcontainers、命名及常见问题的示例与指导。
主要功能
- 01单元、切片和集成测试结构
- 02Spring MVC 与 JPA 切片示例
- 03Testcontainers 数据库配置
- 04测试命名约定
- 05安全与框架常见问题
适用场景
- 01为 Spring 应用各层选择测试类型
- 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 的商品。仅在一次性
测试数据库上运行此脚本。请根据你的应用调整测试数据和响应 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>
常见问题
- Agent 对所有测试都使用
@SpringBootTest——使用切片测试以提升速度 - Agent 在
@DataJpaTest中使用H2内存数据库——使用 Testcontainers 以确保准确性 - Agent 使用
@MockBean——自 Spring Boot 3.4 起已弃用;请使用@MockitoBean(间谍对象请使用@MockitoSpyBean) - Agent 使用
Mockito.mock()而不是@Mock——请结合@ExtendWith(MockitoExtension.class)使用注解 - Agent 忘记在控制器测试中添加
@WithMockUser——安全过滤器会阻止所有请求并返回 401 - Agent 在
@WebMvcTest中发送 POST 请求时未使用.with(csrf())——Boot 的默认安全配置会启用 CSRF,因此即使使用@WithMockUser,每个写入请求也会返回 403 - Agent 使用 JUnit 的
assertEquals——请使用 AssertJ(assertThat(...).isEqualTo(...)) - Agent 将测试命名为
test_createOrder()——请使用createOrder_condition_expected()模式
评论
支持 Markdown