Spring Test详解:从入门到实战
前言
测试是保证软件质量的重要环节。Spring框架提供了强大的测试支持,包括单元测试、集成测试、Web层测试等。本文将深入讲解Spring Test的核心概念、常用注解和实战技巧,帮助开发者编写高质量的测试代码。
一、Spring Test概述
1.1 测试金字塔
测试金字塔
/\
/ \
/ E2E\ ← 端到端测试(最少)
/------\
/ 集成 \ ← 集成测试(适量)
/----------\
/ 单元测试 \ ← 单元测试(最多)
/--------------\
测试类型对比:
┌──────────────┬─────────┬─────────┬─────────┐
│ 测试类型 │ 速度 │ 成本 │ 覆盖率 │
├──────────────┼─────────┼─────────┼─────────┤
│ 单元测试 │ 快 │ 低 │ 高 │
│ 集成测试 │ 中 │ 中 │ 中 │
│ E2E测试 │ 慢 │ 高 │ 低 │
└──────────────┴─────────┴─────────┴─────────┘
1.2 Spring Test模块
Spring Test架构
┌─────────────────────────────────────────┐
│ Spring Test │
│ ┌───────────────────────────────────┐ │
│ │ @SpringBootTest │ │
│ │ @WebMvcTest │ │
│ │ @DataJpaTest │ │
│ │ @MockBean / @SpyBean │ │
│ └───────────────────────────────────┘ │
├─────────────────────────────────────────┤
│ 测试框架 │
│ ┌───────────────────────────────────┐ │
│ │ JUnit 5 / Mockito / AssertJ │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
二、快速开始
2.1 Maven依赖
xml
<dependencies>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 包含以下测试库 -->
<!-- JUnit 5 -->
<!-- Mockito -->
<!-- AssertJ -->
<!-- Hamcrest -->
<!-- JSONassert -->
</dependencies>
2.2 基础单元测试
java
/**
* 计算服务
*/
@Service
public class CalculatorService {
public int add(int a, int b) {
return a + b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("除数不能为0");
}
return a / b;
}
}
/**
* 计算服务单元测试
*/
class CalculatorServiceTest {
private CalculatorService calculator;
@BeforeEach
void setUp() {
calculator = new CalculatorService();
}
@Test
@DisplayName("测试加法")
void testAdd() {
int result = calculator.add(2, 3);
assertEquals(5, result);
}
@Test
@DisplayName("测试除法")
void testDivide() {
int result = calculator.divide(10, 2);
assertEquals(5, result);
}
@Test
@DisplayName("测试除以零抛出异常")
void testDivideByZero() {
assertThrows(IllegalArgumentException.class,
() -> calculator.divide(10, 0));
}
@ParameterizedTest
@CsvSource({"1,1,2", "2,3,5", "10,20,30"})
@DisplayName("参数化测试加法")
void testAddParameterized(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}
}
三、Mockito使用
3.1 Mock基础
java
/**
* 用户服务
*/
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
public User createUser(String username, String email) {
if (userRepository.existsByUsername(username)) {
throw new BusinessException("用户名已存在");
}
User user = User.builder()
.username(username)
.email(email)
.status(UserStatus.ACTIVE)
.build();
User saved = userRepository.save(user);
emailService.sendWelcomeEmail(email);
return saved;
}
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("用户不存在"));
}
}
/**
* 用户服务单元测试
*/
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Test
@DisplayName("创建用户成功")
void testCreateUserSuccess() {
// Given
String username = "zhangsan";
String email = "zhangsan@example.com";
when(userRepository.existsByUsername(username)).thenReturn(false);
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(1L);
return user;
});
// When
User result = userService.createUser(username, email);
// Then
assertNotNull(result);
assertEquals(username, result.getUsername());
assertEquals(email, result.getEmail());
verify(userRepository).existsByUsername(username);
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail(email);
}
@Test
@DisplayName("用户名已存在时抛出异常")
void testCreateUserWithDuplicateUsername() {
// Given
String username = "zhangsan";
when(userRepository.existsByUsername(username)).thenReturn(true);
// When & Then
assertThrows(BusinessException.class,
() -> userService.createUser(username, "test@example.com"));
verify(userRepository, never()).save(any());
verify(emailService, never()).sendWelcomeEmail(anyString());
}
@Test
@DisplayName("查询用户不存在时抛出异常")
void testFindByIdNotFound() {
// Given
Long id = 999L;
when(userRepository.findById(id)).thenReturn(Optional.empty());
// When & Then
assertThrows(EntityNotFoundException.class,
() -> userService.findById(id));
}
}
3.2 Spy与部分Mock
java
/**
* 订单服务
*/
@Service
public class OrderService {
public String generateOrderNo() {
return "ORD" + System.currentTimeMillis();
}
public Order createOrder(Long userId, BigDecimal amount) {
String orderNo = generateOrderNo();
return Order.builder()
.orderNo(orderNo)
.userId(userId)
.amount(amount)
.build();
}
}
/**
* Spy测试
*/
@ExtendWith(MockitoExtension.class)
class OrderServiceSpyTest {
@Spy
private OrderService orderService;
@Test
@DisplayName("使用Spy部分Mock")
void testCreateOrderWithSpy() {
// 只Mock generateOrderNo方法
doReturn("ORD123456").when(orderService).generateOrderNo();
Order order = orderService.createOrder(1L, new BigDecimal("100"));
assertEquals("ORD123456", order.getOrderNo());
assertEquals(1L, order.getUserId());
}
}
3.3 ArgumentCaptor
java
/**
* 使用ArgumentCaptor捕获参数
*/
@ExtendWith(MockitoExtension.class)
class UserServiceCaptorTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Captor
private ArgumentCaptor<User> userCaptor;
@Test
@DisplayName("捕获保存的用户对象")
void testCaptureUserArgument() {
// Given
when(userRepository.existsByUsername(anyString())).thenReturn(false);
when(userRepository.save(any(User.class))).thenAnswer(inv -> {
User u = inv.getArgument(0);
u.setId(1L);
return u;
});
// When
userService.createUser("lisi", "lisi@example.com");
// Then
verify(userRepository).save(userCaptor.capture());
User captured = userCaptor.getValue();
assertEquals("lisi", captured.getUsername());
assertEquals("lisi@example.com", captured.getEmail());
assertEquals(UserStatus.ACTIVE, captured.getStatus());
}
}
四、Spring Boot Test
4.1 @SpringBootTest
java
/**
* 完整集成测试
*/
@SpringBootTest
@Transactional
class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("集成测试:创建用户")
void testCreateUser() {
User user = userService.createUser("testuser", "test@example.com");
assertNotNull(user.getId());
assertTrue(userRepository.existsById(user.getId()));
}
@Test
@DisplayName("集成测试:查询用户")
void testFindById() {
// Given
User saved = userRepository.save(User.builder()
.username("findtest")
.email("find@example.com")
.status(UserStatus.ACTIVE)
.build());
// When
User found = userService.findById(saved.getId());
// Then
assertEquals(saved.getUsername(), found.getUsername());
}
}
/**
* 使用@MockBean替换Bean
*/
@SpringBootTest
class UserServiceMockBeanTest {
@Autowired
private UserService userService;
@MockBean
private EmailService emailService; // Mock掉邮件服务
@Test
@DisplayName("使用MockBean测试")
void testWithMockBean() {
// EmailService被Mock,不会真正发送邮件
doNothing().when(emailService).sendWelcomeEmail(anyString());
User user = userService.createUser("mocktest", "mock@example.com");
assertNotNull(user);
verify(emailService).sendWelcomeEmail("mock@example.com");
}
}
4.2 切片测试
java
/**
* @WebMvcTest - 只测试Web层
*/
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
@DisplayName("测试获取用户")
void testGetUser() throws Exception {
User user = User.builder()
.id(1L)
.username("zhangsan")
.email("zhangsan@example.com")
.build();
when(userService.findById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("zhangsan"))
.andExpect(jsonPath("$.email").value("zhangsan@example.com"));
}
@Test
@DisplayName("测试创建用户")
void testCreateUser() throws Exception {
User user = User.builder()
.id(1L)
.username("newuser")
.email("new@example.com")
.build();
when(userService.createUser(anyString(), anyString())).thenReturn(user);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"newuser\",\"email\":\"new@example.com\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1));
}
@Test
@DisplayName("测试参数校验失败")
void testValidationError() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"\",\"email\":\"invalid\"}"))
.andExpect(status().isBadRequest());
}
}
/**
* @DataJpaTest - 只测试JPA层
*/
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
@DisplayName("测试保存用户")
void testSaveUser() {
User user = User.builder()
.username("repotest")
.email("repo@example.com")
.status(UserStatus.ACTIVE)
.build();
User saved = userRepository.save(user);
assertNotNull(saved.getId());
assertEquals("repotest", saved.getUsername());
}
@Test
@DisplayName("测试按用户名查询")
void testFindByUsername() {
// Given
User user = User.builder()
.username("finduser")
.email("find@example.com")
.status(UserStatus.ACTIVE)
.build();
entityManager.persistAndFlush(user);
// When
Optional<User> found = userRepository.findByUsername("finduser");
// Then
assertTrue(found.isPresent());
assertEquals("find@example.com", found.get().getEmail());
}
@Test
@DisplayName("测试自定义查询")
void testCustomQuery() {
// Given
entityManager.persist(User.builder()
.username("user1").email("u1@test.com").status(UserStatus.ACTIVE).build());
entityManager.persist(User.builder()
.username("user2").email("u2@test.com").status(UserStatus.INACTIVE).build());
entityManager.flush();
// When
List<User> activeUsers = userRepository.findByStatus(UserStatus.ACTIVE);
// Then
assertEquals(1, activeUsers.size());
}
}
五、MockMvc详解
5.1 请求构建
java
/**
* MockMvc请求构建
*/
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Test
@DisplayName("GET请求测试")
void testGet() throws Exception {
mockMvc.perform(get("/api/products")
.param("category", "electronics")
.param("page", "0")
.param("size", "10"))
.andExpect(status().isOk());
}
@Test
@DisplayName("POST请求测试")
void testPost() throws Exception {
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"iPhone\",\"price\":999.99}"))
.andExpect(status().isCreated());
}
@Test
@DisplayName("PUT请求测试")
void testPut() throws Exception {
mockMvc.perform(put("/api/products/1")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"iPhone Pro\",\"price\":1099.99}"))
.andExpect(status().isOk());
}
@Test
@DisplayName("DELETE请求测试")
void testDelete() throws Exception {
mockMvc.perform(delete("/api/products/1"))
.andExpect(status().isNoContent());
}
@Test
@DisplayName("带Header的请求")
void testWithHeader() throws Exception {
mockMvc.perform(get("/api/products")
.header("Authorization", "Bearer token123")
.header("Accept-Language", "zh-CN"))
.andExpect(status().isOk());
}
@Test
@DisplayName("文件上传测试")
void testFileUpload() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file", "test.txt",
MediaType.TEXT_PLAIN_VALUE,
"Hello World".getBytes());
mockMvc.perform(multipart("/api/upload")
.file(file))
.andExpect(status().isOk());
}
}
5.2 响应验证
java
/**
* MockMvc响应验证
*/
@WebMvcTest(UserController.class)
class UserControllerResponseTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
@DisplayName("验证JSON响应")
void testJsonResponse() throws Exception {
User user = User.builder()
.id(1L)
.username("zhangsan")
.email("zhangsan@example.com")
.status(UserStatus.ACTIVE)
.build();
when(userService.findById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
// JSONPath验证
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").value("zhangsan"))
.andExpect(jsonPath("$.email").value("zhangsan@example.com"))
.andExpect(jsonPath("$.status").value("ACTIVE"))
// 验证字段存在
.andExpect(jsonPath("$.id").exists())
// 验证字段不存在
.andExpect(jsonPath("$.password").doesNotExist());
}
@Test
@DisplayName("验证列表响应")
void testListResponse() throws Exception {
List<User> users = Arrays.asList(
User.builder().id(1L).username("user1").build(),
User.builder().id(2L).username("user2").build()
);
when(userService.findAll()).thenReturn(users);
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$.length()").value(2))
.andExpect(jsonPath("$[0].username").value("user1"))
.andExpect(jsonPath("$[1].username").value("user2"));
}
@Test
@DisplayName("验证错误响应")
void testErrorResponse() throws Exception {
when(userService.findById(999L))
.thenThrow(new EntityNotFoundException("用户不存在"));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("用户不存在"));
}
@Test
@DisplayName("打印请求和响应详情")
void testPrintDetails() throws Exception {
when(userService.findById(1L)).thenReturn(
User.builder().id(1L).username("test").build());
mockMvc.perform(get("/api/users/1"))
.andDo(print()) // 打印请求和响应详情
.andExpect(status().isOk());
}
}
六、测试配置
6.1 测试配置文件
yaml
# src/test/resources/application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
mail:
host: localhost
port: 3025
java
/**
* 使用测试Profile
*/
@SpringBootTest
@ActiveProfiles("test")
class IntegrationTest {
@Test
void testWithTestProfile() {
// 使用application-test.yml配置
}
}
6.2 测试数据准备
java
/**
* 使用@Sql准备数据
*/
@SpringBootTest
@Sql(scripts = "/sql/init-users.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
class SqlAnnotationTest {
@Autowired
private UserRepository userRepository;
@Test
void testWithPreparedData() {
List<User> users = userRepository.findAll();
assertFalse(users.isEmpty());
}
}
/**
* 使用TestEntityManager
*/
@DataJpaTest
class TestEntityManagerTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
entityManager.persist(User.builder()
.username("testuser")
.email("test@example.com")
.status(UserStatus.ACTIVE)
.build());
entityManager.flush();
}
@Test
void testFind() {
Optional<User> user = userRepository.findByUsername("testuser");
assertTrue(user.isPresent());
}
}
七、实战案例
7.1 案例1:完整的Service测试
java
/**
* 订单服务
*/
@Service
@Transactional
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private InventoryService inventoryService;
@Autowired
private PaymentService paymentService;
public Order createOrder(CreateOrderRequest request) {
// 验证用户
User user = userRepository.findById(request.getUserId())
.orElseThrow(() -> new EntityNotFoundException("用户不存在"));
// 检查库存
if (!inventoryService.checkStock(request.getProductId(), request.getQuantity())) {
throw new BusinessException("库存不足");
}
// 创建订单
Order order = Order.builder()
.orderNo(generateOrderNo())
.user(user)
.productId(request.getProductId())
.quantity(request.getQuantity())
.amount(request.getAmount())
.status(OrderStatus.PENDING)
.build();
return orderRepository.save(order);
}
public Order payOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new EntityNotFoundException("订单不存在"));
if (order.getStatus() != OrderStatus.PENDING) {
throw new BusinessException("订单状态不正确");
}
// 调用支付
boolean success = paymentService.pay(order.getAmount());
if (!success) {
throw new BusinessException("支付失败");
}
// 扣减库存
inventoryService.deductStock(order.getProductId(), order.getQuantity());
order.setStatus(OrderStatus.PAID);
return orderRepository.save(order);
}
private String generateOrderNo() {
return "ORD" + System.currentTimeMillis();
}
}
/**
* 订单服务测试
*/
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private UserRepository userRepository;
@Mock
private InventoryService inventoryService;
@Mock
private PaymentService paymentService;
@InjectMocks
private OrderService orderService;
private User testUser;
private CreateOrderRequest createRequest;
@BeforeEach
void setUp() {
testUser = User.builder()
.id(1L)
.username("testuser")
.build();
createRequest = new CreateOrderRequest();
createRequest.setUserId(1L);
createRequest.setProductId(100L);
createRequest.setQuantity(2);
createRequest.setAmount(new BigDecimal("199.99"));
}
@Nested
@DisplayName("创建订单测试")
class CreateOrderTests {
@Test
@DisplayName("成功创建订单")
void testCreateOrderSuccess() {
// Given
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
when(inventoryService.checkStock(100L, 2)).thenReturn(true);
when(orderRepository.save(any(Order.class))).thenAnswer(inv -> {
Order order = inv.getArgument(0);
order.setId(1L);
return order;
});
// When
Order result = orderService.createOrder(createRequest);
// Then
assertNotNull(result);
assertEquals(OrderStatus.PENDING, result.getStatus());
verify(orderRepository).save(any(Order.class));
}
@Test
@DisplayName("用户不存在时抛出异常")
void testCreateOrderUserNotFound() {
when(userRepository.findById(1L)).thenReturn(Optional.empty());
assertThrows(EntityNotFoundException.class,
() -> orderService.createOrder(createRequest));
verify(orderRepository, never()).save(any());
}
@Test
@DisplayName("库存不足时抛出异常")
void testCreateOrderInsufficientStock() {
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
when(inventoryService.checkStock(100L, 2)).thenReturn(false);
assertThrows(BusinessException.class,
() -> orderService.createOrder(createRequest));
}
}
@Nested
@DisplayName("支付订单测试")
class PayOrderTests {
@Test
@DisplayName("成功支付订单")
void testPayOrderSuccess() {
Order order = Order.builder()
.id(1L)
.productId(100L)
.quantity(2)
.amount(new BigDecimal("199.99"))
.status(OrderStatus.PENDING)
.build();
when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
when(paymentService.pay(any())).thenReturn(true);
when(orderRepository.save(any())).thenReturn(order);
Order result = orderService.payOrder(1L);
assertEquals(OrderStatus.PAID, result.getStatus());
verify(inventoryService).deductStock(100L, 2);
}
@Test
@DisplayName("支付失败时抛出异常")
void testPayOrderPaymentFailed() {
Order order = Order.builder()
.id(1L)
.status(OrderStatus.PENDING)
.amount(new BigDecimal("199.99"))
.build();
when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
when(paymentService.pay(any())).thenReturn(false);
assertThrows(BusinessException.class,
() -> orderService.payOrder(1L));
verify(inventoryService, never()).deductStock(anyLong(), anyInt());
}
}
}
7.2 案例2:Controller集成测试
java
/**
* 用户Controller
*/
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(convertToDTO(user));
}
@GetMapping
public ResponseEntity<Page<UserDTO>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Page<User> users = userService.findAll(PageRequest.of(page, size));
Page<UserDTO> dtoPage = users.map(this::convertToDTO);
return ResponseEntity.ok(dtoPage);
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(convertToDTO(user));
}
private UserDTO convertToDTO(User user) {
return UserDTO.builder()
.id(user.getId())
.username(user.getUsername())
.email(user.getEmail())
.build();
}
}
/**
* Controller集成测试
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
@DisplayName("集成测试:创建并获取用户")
void testCreateAndGetUser() {
// 创建用户
CreateUserRequest request = new CreateUserRequest();
request.setUsername("integrationtest");
request.setEmail("integration@example.com");
request.setPassword("password123");
ResponseEntity<UserDTO> createResponse = restTemplate.postForEntity(
"/api/users", request, UserDTO.class);
assertEquals(HttpStatus.CREATED, createResponse.getStatusCode());
assertNotNull(createResponse.getBody());
Long userId = createResponse.getBody().getId();
// 获取用户
ResponseEntity<UserDTO> getResponse = restTemplate.getForEntity(
"/api/users/" + userId, UserDTO.class);
assertEquals(HttpStatus.OK, getResponse.getStatusCode());
assertEquals("integrationtest", getResponse.getBody().getUsername());
}
@Test
@DisplayName("集成测试:分页查询")
void testGetUsersWithPagination() {
// 准备数据
for (int i = 0; i < 15; i++) {
userRepository.save(User.builder()
.username("user" + i)
.email("user" + i + "@example.com")
.status(UserStatus.ACTIVE)
.build());
}
// 查询第一页
ResponseEntity<String> response = restTemplate.getForEntity(
"/api/users?page=0&size=10", String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
}
7.3 案例3:异步方法测试
java
/**
* 异步服务
*/
@Service
public class AsyncService {
@Async
public CompletableFuture<String> asyncProcess(String input) {
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return CompletableFuture.completedFuture("Processed: " + input);
}
}
/**
* 异步方法测试
*/
@SpringBootTest
@EnableAsync
class AsyncServiceTest {
@Autowired
private AsyncService asyncService;
@Test
@DisplayName("测试异步方法")
void testAsyncProcess() throws Exception {
CompletableFuture<String> future = asyncService.asyncProcess("test");
String result = future.get(5, TimeUnit.SECONDS);
assertEquals("Processed: test", result);
}
@Test
@DisplayName("测试多个异步调用")
void testMultipleAsyncCalls() throws Exception {
CompletableFuture<String> future1 = asyncService.asyncProcess("input1");
CompletableFuture<String> future2 = asyncService.asyncProcess("input2");
CompletableFuture<String> future3 = asyncService.asyncProcess("input3");
CompletableFuture.allOf(future1, future2, future3).join();
assertEquals("Processed: input1", future1.get());
assertEquals("Processed: input2", future2.get());
assertEquals("Processed: input3", future3.get());
}
}
八、最佳实践
8.1 测试命名与组织
java
/**
* 测试命名规范
*/
class UserServiceTest {
// 方法名格式:test_方法名_场景_预期结果
@Test
void createUser_withValidInput_returnsCreatedUser() { }
@Test
void createUser_withDuplicateUsername_throwsException() { }
@Test
void findById_withExistingId_returnsUser() { }
@Test
void findById_withNonExistingId_throwsNotFoundException() { }
// 使用@Nested分组相关测试
@Nested
@DisplayName("创建用户测试")
class CreateUserTests {
@Test
@DisplayName("正常创建")
void success() { }
@Test
@DisplayName("用户名为空")
void emptyUsername() { }
}
}
8.2 测试隔离
java
/**
* 测试隔离最佳实践
*/
@SpringBootTest
@Transactional // 每个测试后自动回滚
class IsolatedTest {
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
// 清理数据确保隔离
userRepository.deleteAll();
}
@Test
void test1() {
userRepository.save(User.builder().username("test1").build());
assertEquals(1, userRepository.count());
}
@Test
void test2() {
// 由于@Transactional回滚,test1的数据不会影响test2
assertEquals(0, userRepository.count());
}
}
8.3 测试覆盖率
xml
<!-- pom.xml - JaCoCo配置 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
九、总结
核心知识点回顾
Spring Test核心要点
│
├── 测试类型
│ ├── 单元测试
│ ├── 集成测试
│ └── 切片测试
│
├── 核心注解
│ ├── @SpringBootTest
│ ├── @WebMvcTest
│ ├── @DataJpaTest
│ ├── @MockBean
│ └── @SpyBean
│
├── Mockito使用
│ ├── @Mock / @InjectMocks
│ ├── when().thenReturn()
│ ├── verify()
│ └── ArgumentCaptor
│
├── MockMvc
│ ├── 请求构建
│ ├── 响应验证
│ └── JSONPath
│
├── 测试配置
│ ├── 测试Profile
│ ├── @Sql数据准备
│ └── TestEntityManager
│
└── 最佳实践
├── 命名规范
├── 测试隔离
└── 覆盖率
良好的测试是高质量软件的保障。掌握Spring Test的使用技巧,能够帮助我们编写可靠、可维护的测试代码,提高项目质量。