Spring Test详解

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的使用技巧,能够帮助我们编写可靠、可维护的测试代码,提高项目质量。


相关推荐
Java水解1 小时前
常用经典 SQL 语句大全完整版–详解+实例
后端
神奇小汤圆1 小时前
MQ生产者确认机制捕获到消息投递失败后如何重试?
后端
不思念一个荒废的名字1 小时前
【黑马JavaWeb+AI知识梳理】Web后端开发01 - 准备工作、部门管理、日志技术、多表关系、员工管理
后端
sugar__salt1 小时前
网络编程套接字(二)——TCP
java·网络·网络协议·tcp/ip·java-ee·javaee
颜颜yan_1 小时前
跨越x86与ARM:openEuler全架构算力实战评测
java·arm开发·架构
毕设源码-朱学姐1 小时前
【开题答辩全过程】以 陪诊就医小程序设计与实现为例,包含答辩的问题和答案
java
用户68545375977691 小时前
别再裸奔写Python了!类型注解+mypy让你代码健壮如钢铁侠
后端
用户68545375977691 小时前
为什么大厂都在升级Python 3.12?看完我连夜重构了代码
后端
Frank_zhou1 小时前
039_Netty网络编程服务端入门程序开发
后端