Spring 测试框架深度使用¶
概述¶
Spring 测试框架提供了强大的测试支持,从单元测试到集成测试,从 Mock 对象到测试切片。本文深度解析 Spring 测试框架的高级用法。
graph TB
A[Spring 测试框架] --> B[测试类型]
A --> C[Mock 技术]
A --> D[测试策略]
A --> E[最佳实践]
B --> B1[单元测试]
B --> B2[集成测试]
B --> B3[Web 测试]
B --> B4[数据访问测试]
C --> C1[Mockito]
C --> C2[Spy 对象]
C --> C3[参数匹配器]
C --> C4[行为验证]
D --> D1[测试金字塔]
D --> D2[契约测试]
D --> D3[性能测试]
E --> E1[测试数据管理]
E --> E2[测试隔离]
E --> E3[测试报告] Spring 测试框架核心¶
1. 测试注解体系¶
核心测试注解¶
// 基础测试注解
@SpringBootTest // 启动完整的 Spring 应用上下文
@WebMvcTest // 仅测试 Web 层,不加载完整的上下文
@DataJpaTest // 仅测试 JPA 组件
@JsonTest // 仅测试 JSON 序列化/反序列化
@RestClientTest // 仅测试 REST 客户端
测试配置注解¶
// 配置相关注解
@TestConfiguration // 测试专用的配置类
@ContextConfiguration // 指定配置文件
@ActiveProfiles // 激活特定的 Profile
@TestPropertySource // 加载测试属性文件
@Import // 导入特定的配置类
2. 测试切片(Test Slices)¶
Web 层测试切片¶
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUserWhenValidId() throws Exception {
// 模拟服务层返回
given(userService.getUserById(1L))
.willReturn(new User(1L, "John", "[email protected]"));
// 执行 HTTP 请求并验证
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
given(userService.getUserById(999L))
.willThrow(new UserNotFoundException("User not found"));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
}
数据访问层测试切片¶
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void shouldFindUserByEmail() {
// 准备测试数据
User user = new User("John", "[email protected]");
entityManager.persist(user);
entityManager.flush();
// 执行查询
Optional<User> found = userRepository.findByEmail("[email protected]");
// 验证结果
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("John");
}
@Test
void shouldReturnEmptyWhenEmailNotFound() {
Optional<User> found = userRepository.findByEmail("[email protected]");
assertThat(found).isEmpty();
}
}
Mock 技术深度使用¶
1. Mockito 高级特性¶
参数匹配器(Argument Matchers)¶
@Service
class OrderService {
public Order createOrder(Long userId, OrderRequest request) {
// 业务逻辑
return order;
}
public List<Order> findOrdersByStatus(Long userId, OrderStatus status) {
// 查询逻辑
return orders;
}
}
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@InjectMocks
private OrderService orderService;
@Test
void shouldCreateOrderWithAnyUserId() {
OrderRequest request = new OrderRequest("product1", 2);
Order expectedOrder = new Order(1L, 100L, request);
// 使用 any() 匹配器,不关心具体的 userId
given(orderRepository.save(any(Order.class)))
.willReturn(expectedOrder);
Order result = orderService.createOrder(anyLong(), request);
assertThat(result).isEqualTo(expectedOrder);
verify(orderRepository).save(any(Order.class));
}
@Test
void shouldFindOrdersWithSpecificStatus() {
List<Order> pendingOrders = Arrays.asList(
new Order(1L, 100L, OrderStatus.PENDING),
new Order(2L, 100L, OrderStatus.PENDING)
);
// 使用 eq() 精确匹配参数
given(orderRepository.findByUserIdAndStatus(eq(100L), eq(OrderStatus.PENDING)))
.willReturn(pendingOrders);
List<Order> result = orderService.findOrdersByStatus(100L, OrderStatus.PENDING);
assertThat(result).hasSize(2);
assertThat(result).allMatch(order -> order.getStatus() == OrderStatus.PENDING);
}
@Test
void shouldHandleComplexParameterMatching() {
// 使用 argThat() 自定义匹配器
given(orderRepository.save(argThat(order ->
order.getUserId() != null && order.getTotalAmount().compareTo(BigDecimal.ZERO) > 0)))
).willAnswer(invocation -> invocation.getArgument(0));
OrderRequest request = new OrderRequest("product1", 2);
Order result = orderService.createOrder(100L, request);
assertThat(result).isNotNull();
assertThat(result.getUserId()).isEqualTo(100L);
}
}
Spy 对象使用¶
@Service
class PaymentService {
public PaymentResult processPayment(PaymentRequest request) {
validatePayment(request);
PaymentResult result = callPaymentGateway(request);
recordPayment(result);
return result;
}
protected void validatePayment(PaymentRequest request) {
if (request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Payment amount must be positive");
}
}
protected PaymentResult callPaymentGateway(PaymentRequest request) {
// 调用第三方支付网关
return paymentGateway.process(request);
}
protected void recordPayment(PaymentResult result) {
// 记录支付结果
paymentRepository.save(result);
}
}
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
@Spy
@InjectMocks
private PaymentService paymentService;
@Mock
private PaymentGateway paymentGateway;
@Mock
private PaymentRepository paymentRepository;
@Test
void shouldProcessPaymentSuccessfully() {
PaymentRequest request = new PaymentRequest(BigDecimal.valueOf(100));
PaymentResult expectedResult = new PaymentResult("SUCCESS", "12345");
// 模拟第三方调用
given(paymentGateway.process(request)).willReturn(expectedResult);
// 执行测试
PaymentResult result = paymentService.processPayment(request);
// 验证结果
assertThat(result.getStatus()).isEqualTo("SUCCESS");
// 验证方法调用
verify(paymentService).validatePayment(request);
verify(paymentService).callPaymentGateway(request);
verify(paymentService).recordPayment(expectedResult);
verify(paymentRepository).save(expectedResult);
}
@Test
void shouldSkipGatewayCallWhenValidationFails() {
PaymentRequest invalidRequest = new PaymentRequest(BigDecimal.valueOf(-100));
// 验证异常抛出
assertThatThrownBy(() -> paymentService.processPayment(invalidRequest))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Payment amount must be positive");
// 验证网关未被调用
verify(paymentGateway, never()).process(any());
verify(paymentRepository, never()).save(any());
}
@Test
void shouldStubSpecificMethod() {
PaymentRequest request = new PaymentRequest(BigDecimal.valueOf(100));
// 对 Spy 对象的特定方法进行存根
doReturn(new PaymentResult("MOCKED", "mock-id"))
.when(paymentService).callPaymentGateway(request);
PaymentResult result = paymentService.processPayment(request);
assertThat(result.getStatus()).isEqualTo("MOCKED");
// 验证存根方法被调用,而不是原始实现
verify(paymentService).callPaymentGateway(request);
verify(paymentGateway, never()).process(any());
}
}
2. 行为验证高级用法¶
调用次数验证¶
@ExtendWith(MockitoExtension.class)
class NotificationServiceTest {
@Mock
private EmailService emailService;
@Mock
private SmsService smsService;
@InjectMocks
private NotificationService notificationService;
@Test
void shouldSendBothEmailAndSmsForImportantNotification() {
NotificationRequest request = new NotificationRequest("重要通知", "这是一条重要消息", true);
notificationService.sendNotification(request);
// 验证方法调用次数
verify(emailService, times(1)).sendEmail(anyString(), anyString());
verify(smsService, times(1)).sendSms(anyString(), anyString());
// 验证调用顺序
InOrder inOrder = inOrder(emailService, smsService);
inOrder.verify(emailService).sendEmail(anyString(), anyString());
inOrder.verify(smsService).sendSms(anyString(), anyString());
}
@Test
void shouldSendOnlyEmailForNormalNotification() {
NotificationRequest request = new NotificationRequest("普通通知", "这是一条普通消息", false);
notificationService.sendNotification(request);
// 验证邮件发送一次,短信不发送
verify(emailService, times(1)).sendEmail(anyString(), anyString());
verify(smsService, never()).sendSms(anyString(), anyString());
}
@Test
void shouldHandleRetryLogic() {
NotificationRequest request = new NotificationRequest("重试测试", "测试消息", true);
// 第一次调用失败,第二次成功
given(emailService.sendEmail(anyString(), anyString()))
.willThrow(new RuntimeException("Network error"))
.willReturn(true);
notificationService.sendNotificationWithRetry(request, 3);
// 验证重试逻辑(总共调用2次)
verify(emailService, times(2)).sendEmail(anyString(), anyString());
}
@Test
void shouldVerifyWithTimeout() {
NotificationRequest request = new NotificationRequest("异步通知", "异步消息", false);
// 异步发送通知
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(100); // 模拟异步延迟
notificationService.sendNotification(request);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 使用超时验证异步调用
verify(emailService, timeout(500).times(1)).sendEmail(anyString(), anyString());
}
}
集成测试深度实践¶
1. @SpringBootTest 高级用法¶
测试配置管理¶
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"spring.jpa.hibernate.ddl-auto=create-drop",
"logging.level.org.springframework=DEBUG"
}
)
@ActiveProfiles("test")
@TestPropertySource(locations = "classpath:application-test.properties")
class UserServiceIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@LocalServerPort
private int port;
@BeforeEach
void setUp() {
// 清理测试数据
userRepository.deleteAll();
// 准备测试数据
User user1 = new User("Alice", "[email protected]");
User user2 = new User("Bob", "[email protected]");
userRepository.saveAll(Arrays.asList(user1, user2));
}
@Test
void shouldGetAllUsersViaRest() {
ResponseEntity<User[]> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/users", User[].class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).hasSize(2);
assertThat(response.getBody())
.extracting(User::getName)
.containsExactlyInAnyOrder("Alice", "Bob");
}
@Test
void shouldCreateUserViaRest() {
User newUser = new User("Charlie", "[email protected]");
ResponseEntity<User> response = restTemplate.postForEntity(
"http://localhost:" + port + "/api/users", newUser, User.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getId()).isNotNull();
assertThat(response.getBody().getName()).isEqualTo("Charlie");
// 验证数据库中的数据
assertThat(userRepository.count()).isEqualTo(3);
assertThat(userRepository.findByEmail("[email protected]")).isPresent();
}
@Test
@Sql(scripts = "/sql/test-users.sql")
@Sql(scripts = "/sql/cleanup-users.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void shouldLoadTestDataFromSqlScript() {
// 测试数据由 @Sql 注解加载
List<User> users = userRepository.findAll();
assertThat(users).hasSize(5); // 脚本中插入了5条记录
Optional<User> admin = userRepository.findByEmail("[email protected]");
assertThat(admin).isPresent();
assertThat(admin.get().getName()).isEqualTo("Admin User");
}
}
测试事务管理¶
@SpringBootTest
@Transactional
class TransactionalUserServiceTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
void shouldRollbackTransactionOnFailure() {
// 准备测试数据
User user1 = new User("User1", "[email protected]");
User user2 = new User("User2", "[email protected]");
userRepository.save(user1);
// 这个操作会失败,导致事务回滚
assertThatThrownBy(() -> userService.batchCreateUsers(Arrays.asList(user1, user2)))
.isInstanceOf(DataIntegrityViolationException.class);
// 验证事务回滚,user1 也没有被保存
assertThat(userRepository.count()).isEqualTo(0);
}
@Test
@Rollback(false) // 禁用自动回滚
void shouldCommitTransactionWhenDisabledRollback() {
User user = new User("TestUser", "[email protected]");
userService.createUser(user);
// 验证数据被提交到数据库
assertThat(userRepository.count()).isEqualTo(1);
assertThat(userRepository.findByEmail("[email protected]")).isPresent();
// 手动清理
userRepository.deleteAll();
}
@Test
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
void shouldResetApplicationContext() {
// 这个测试会修改应用上下文状态
userService.updateCacheConfiguration("new-config");
// 由于使用了 @DirtiesContext,下一个测试会使用干净的应用上下文
}
}
2. 测试数据管理¶
使用 Testcontainers¶
@Testcontainers
@SpringBootTest
class UserServiceWithTestcontainersTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldWorkWithRealDatabase() {
User user = new User("Test", "[email protected]");
User saved = userRepository.save(user);
assertThat(saved.getId()).isNotNull();
assertThat(userRepository.count()).isEqualTo(1);
}
}
使用@DataJpaTest 与 Testcontainers 结合¶
@DataJpaTest
@Testcontainers
class UserRepositoryWithTestcontainersTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void shouldPersistUserWithTestcontainers() {
User user = new User("John", "[email protected]");
User saved = userRepository.save(user);
assertThat(saved.getId()).isNotNull();
assertThat(userRepository.findByEmail("[email protected]")).isPresent();
}
}
测试策略与最佳实践¶
1. 测试金字塔实践¶
graph TB
A[测试金字塔] --> B[单元测试]
A --> C[集成测试]
A --> D[端到端测试]
B --> B1[数量最多]
B --> B2[执行最快]
B --> B3[隔离性最好]
C --> C1[数量适中]
C --> C2[执行中等]
C --> C3[验证集成]
D --> D1[数量最少]
D --> D2[执行最慢]
D --> D3[验证流程] 测试分布建议¶
// 单元测试示例(70%)
class UserServiceUnitTest {
@Test
void shouldCalculateUserAge() {
// 纯业务逻辑测试,不依赖外部资源
}
}
// 集成测试示例(20%)
@WebMvcTest(UserController.class)
class UserControllerIntegrationTest {
@Test
void shouldReturnUserViaHttp() {
// 测试控制器与HTTP层的集成
}
}
// 端到端测试示例(10%)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserServiceE2ETest {
@Test
void shouldCompleteUserRegistrationFlow() {
// 测试完整的用户注册流程
}
}
2. 契约测试¶
Spring Cloud Contract 使用¶
// 生产者端契约定义
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class UserContractTest {
@Autowired
private UserController userController;
@Before
public void setup() {
StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(userController);
RestAssuredMockMvc.standaloneSetup(builder);
}
@Test
public void shouldReturnUserWhenExists() {
given()
.mockMvc(mockMvc)
.when()
.get("/users/1")
.then()
.status(200)
.body("id", equalTo(1))
.body("name", equalTo("John"));
}
}
// 消费者端测试
@SpringBootTest
@AutoConfigureStubRunner(ids = {"com.example:user-service:+:stubs:8080"})
class UserServiceConsumerTest {
@Autowired
private UserServiceClient userServiceClient;
@Test
void shouldGetUserFromProducer() {
User user = userServiceClient.getUser(1L);
assertThat(user.getId()).isEqualTo(1L);
assertThat(user.getName()).isEqualTo("John");
}
}
3. 性能测试¶
使用 @RepeatedTest 进行性能基准测试¶
@SpringBootTest
class UserServicePerformanceTest {
@Autowired
private UserService userService;
@RepeatedTest(100) // 重复执行100次
void shouldProcessUserQuickly() {
long startTime = System.nanoTime();
User user = userService.createUser(new User("Test", "[email protected]"));
long duration = System.nanoTime() - startTime;
// 断言执行时间在合理范围内
assertThat(duration).isLessThan(100_000_000); // 100ms
assertThat(user).isNotNull();
}
@Test
void shouldHandleConcurrentRequests() throws InterruptedException {
int threadCount = 10;
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
startLatch.await();
userService.createUser(new User("User" + Thread.currentThread().getId(), "[email protected]"));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
endLatch.countDown();
}
});
futures.add(future);
}
// 同时启动所有线程
startLatch.countDown();
// 等待所有线程完成
endLatch.await(10, TimeUnit.SECONDS);
// 验证所有任务都成功完成
for (CompletableFuture<Void> future : futures) {
assertThat(future).isDone();
assertThat(future).isNotCompletedExceptionally();
}
}
}
总结¶
Spring 测试框架提供了丰富的测试支持:
- 测试切片:针对不同层次的精准测试
- Mock 技术:强大的 Mockito 集成和高级用法
- 集成测试:完整的应用上下文测试支持
- 测试策略:测试金字塔、契约测试、性能测试
- 最佳实践:测试数据管理、事务控制、测试隔离
通过深度使用 Spring 测试框架,可以构建可靠、可维护的测试套件,确保代码质量和系统稳定性。