Skip to content

Spring Boot 测试

1. 测试概述

1.1 测试类型

类型说明范围
单元测试测试单个方法或类最小范围
集成测试测试多个组件协作中等范围
端到端测试测试完整业务流程最大范围

1.2 测试依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

2. 单元测试

2.1 JUnit 5 基础

java
@DisplayName("计算器测试")
class CalculatorTest {
    
    private Calculator calculator;
    
    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }
    
    @Test
    @DisplayName("加法测试")
    void testAdd() {
        int result = calculator.add(2, 3);
        assertEquals(5, result);
    }
    
    @Test
    @DisplayName("除法异常测试")
    void testDivideByZero() {
        assertThrows(ArithmeticException.class, () -> {
            calculator.divide(10, 0);
        });
    }
    
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    @DisplayName("参数化测试")
    void testIsPositive(int number) {
        assertTrue(calculator.isPositive(number));
    }
    
    @Nested
    @DisplayName("嵌套测试")
    class NestedTests {
        @Test
        void testNested() {
            assertTrue(true);
        }
    }
}

2.2 Mockito Mock

java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void testFindById() {
        User user = new User(1L, "张三", "zhangsan@example.com");
        
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        
        User result = userService.findById(1L);
        
        assertNotNull(result);
        assertEquals("张三", result.getUsername());
        verify(userRepository, times(1)).findById(1L);
    }
    
    @Test
    void testCreate() {
        User user = new User(null, "李四", "lisi@example.com");
        User savedUser = new User(1L, "李四", "lisi@example.com");
        
        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        
        User result = userService.create(user);
        
        assertNotNull(result.getId());
        verify(userRepository).save(user);
    }
    
    @Test
    void testDelete() {
        doNothing().when(userRepository).deleteById(1L);
        
        userService.delete(1L);
        
        verify(userRepository).deleteById(1L);
    }
}

2.3 AssertJ 断言

java
import static org.assertj.core.api.Assertions.*;

class UserTest {
    
    @Test
    void testUserAssertions() {
        User user = new User(1L, "张三", "zhangsan@example.com");
        
        assertThat(user)
            .isNotNull()
            .hasFieldOrProperty("id")
            .hasFieldOrPropertyWithValue("username", "张三");
        
        assertThat(user.getUsername())
            .isNotEmpty()
            .startsWith("张")
            .endsWith("三")
            .hasSize(2);
        
        assertThat(user.getEmail())
            .contains("@")
            .endsWith(".com");
    }
    
    @Test
    void testListAssertions() {
        List<User> users = List.of(
            new User(1L, "张三", "zhangsan@example.com"),
            new User(2L, "李四", "lisi@example.com")
        );
        
        assertThat(users)
            .hasSize(2)
            .extracting("username")
            .containsExactly("张三", "李四");
    }
}

3. 集成测试

3.1 @SpringBootTest

java
@SpringBootTest
class UserServiceIntegrationTest {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private UserRepository userRepository;
    
    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }
    
    @Test
    void testCreateAndFind() {
        User user = new User(null, "张三", "zhangsan@example.com");
        
        User saved = userService.create(user);
        
        assertThat(saved.getId()).isNotNull();
        
        User found = userService.findById(saved.getId());
        
        assertThat(found).isNotNull();
        assertThat(found.getUsername()).isEqualTo("张三");
    }
}

3.2 测试配置

yaml
# src/test/resources/application-test.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
java
@SpringBootTest
@ActiveProfiles("test")
class UserServiceTest {
    // 使用 test profile 配置
}

3.3 测试数据库

java
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testFindByUsername() {
        User user = new User(null, "张三", "zhangsan@example.com");
        entityManager.persistAndFlush(user);
        
        Optional<User> found = userRepository.findByUsername("张三");
        
        assertThat(found).isPresent();
        assertThat(found.get().getEmail()).isEqualTo("zhangsan@example.com");
    }
    
    @Test
    void testExistsByUsername() {
        User user = new User(null, "张三", "zhangsan@example.com");
        entityManager.persistAndFlush(user);
        
        boolean exists = userRepository.existsByUsername("张三");
        
        assertThat(exists).isTrue();
    }
}

4. Web 层测试

4.1 @WebMvcTest

java
@WebMvcTest(UserController.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void testGetById() throws Exception {
        User user = new User(1L, "张三", "zhangsan@example.com");
        
        when(userService.findById(1L)).thenReturn(user);
        
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.username").value("张三"))
            .andExpect(jsonPath("$.email").value("zhangsan@example.com"));
    }
    
    @Test
    void testCreate() throws Exception {
        User user = new User(1L, "张三", "zhangsan@example.com");
        String userJson = """
            {
                "username": "张三",
                "email": "zhangsan@example.com"
            }
            """;
        
        when(userService.create(any(User.class))).thenReturn(user);
        
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userJson))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.username").value("张三"));
    }
    
    @Test
    void testGetAll() throws Exception {
        List<User> users = List.of(
            new User(1L, "张三", "zhangsan@example.com"),
            new User(2L, "李四", "lisi@example.com")
        );
        
        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("张三"));
    }
}

4.2 @AutoConfigureMockMvc

java
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private UserRepository userRepository;
    
    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }
    
    @Test
    void testCreateAndGet() throws Exception {
        String userJson = """
            {
                "username": "张三",
                "email": "zhangsan@example.com"
            }
            """;
        
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userJson))
            .andExpect(status().isCreated());
        
        mockMvc.perform(get("/api/users"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].username").value("张三"));
    }
}

4.3 安全测试

java
@SpringBootTest
@AutoConfigureMockMvc
class SecurityTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    @WithMockUser(username = "admin", roles = {"ADMIN"})
    void testAdminEndpoint() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
    }
    
    @Test
    @WithMockUser(username = "user", roles = {"USER"})
    void testAdminEndpointForbidden() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isForbidden());
    }
    
    @Test
    void testUnauthorized() throws Exception {
        mockMvc.perform(get("/api/users"))
            .andExpect(status().isUnauthorized());
    }
    
    @Test
    @WithUserDetails("admin")
    void testWithUserDetails() throws Exception {
        mockMvc.perform(get("/api/profile"))
            .andExpect(status().isOk());
    }
}

5. 测试事务

5.1 @Transactional

java
@SpringBootTest
@Transactional
class TransactionalTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testCreate() {
        User user = new User(null, "张三", "zhangsan@example.com");
        userRepository.save(user);
        
        assertThat(user.getId()).isNotNull();
    }
    
    @Test
    void testRollback() {
        long count = userRepository.count();
        
        User user = new User(null, "张三", "zhangsan@example.com");
        userRepository.save(user);
        
        assertThat(userRepository.count()).isEqualTo(count + 1);
    }
}

5.2 @Commit 和 @Rollback

java
@SpringBootTest
class CommitRollbackTest {
    
    @Test
    @Transactional
    @Rollback
    void testRollback() {
        // 测试后回滚
    }
    
    @Test
    @Transactional
    @Commit
    void testCommit() {
        // 测试后提交
    }
}

6. 测试切片

6.1 @DataJpaTest

java
@DataJpaTest
class JpaSliceTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testRepository() {
        User user = new User(null, "张三", "zhangsan@example.com");
        userRepository.save(user);
        
        assertThat(userRepository.count()).isEqualTo(1);
    }
}

6.2 @DataRedisTest

java
@DataRedisTest
class RedisSliceTest {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Test
    void testRedis() {
        redisTemplate.opsForValue().set("test", "value");
        
        assertThat(redisTemplate.opsForValue().get("test")).isEqualTo("value");
    }
}

6.3 @JsonTest

java
@JsonTest
class JsonTest {
    
    @Autowired
    private JacksonTester<User> jsonTester;
    
    @Test
    void testSerialize() throws IOException {
        User user = new User(1L, "张三", "zhangsan@example.com");
        
        JsonContent<User> result = jsonTester.write(user);
        
        assertThat(result).hasJsonPathStringValue("$.username");
        assertThat(result).extractingJsonPathStringValue("$.username").isEqualTo("张三");
    }
    
    @Test
    void testDeserialize() throws IOException {
        String json = """
            {
                "id": 1,
                "username": "张三",
                "email": "zhangsan@example.com"
            }
            """;
        
        User result = jsonTester.parseObject(json);
        
        assertThat(result.getUsername()).isEqualTo("张三");
    }
}

7. 测试工具

7.1 TestRestTemplate

java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RestTemplateTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @LocalServerPort
    private int port;
    
    @Test
    void testGetUser() {
        ResponseEntity<User> response = restTemplate.getForEntity(
            "/api/users/1", User.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isNotNull();
    }
    
    @Test
    void testCreateUser() {
        User user = new User(null, "张三", "zhangsan@example.com");
        
        ResponseEntity<User> response = restTemplate.postForEntity(
            "/api/users", user, User.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
    }
}

7.2 WebTestClient

java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WebTestClientTest {
    
    @Autowired
    private WebTestClient webTestClient;
    
    @Test
    void testGetUser() {
        webTestClient.get()
            .uri("/api/users/1")
            .accept(MediaType.APPLICATION_JSON)
            .exchange()
            .expectStatus().isOk()
            .expectBody(User.class)
            .value(user -> assertThat(user.getUsername()).isEqualTo("张三"));
    }
    
    @Test
    void testCreateUser() {
        User user = new User(null, "张三", "zhangsan@example.com");
        
        webTestClient.post()
            .uri("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(user)
            .exchange()
            .expectStatus().isCreated()
            .expectBody()
            .jsonPath("$.username").isEqualTo("张三");
    }
}

8. 测试最佳实践

8.1 测试命名

java
class UserServiceTest {
    
    @Test
    void findById_WhenUserExists_ReturnsUser() {
        // Given
        User user = new User(1L, "张三", "zhangsan@example.com");
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        
        // When
        User result = userService.findById(1L);
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.getUsername()).isEqualTo("张三");
    }
    
    @Test
    void findById_WhenUserNotExists_ReturnsNull() {
        // Given
        when(userRepository.findById(1L)).thenReturn(Optional.empty());
        
        // When
        User result = userService.findById(1L);
        
        // Then
        assertThat(result).isNull();
    }
}

8.2 测试数据

java
class TestDataFactory {
    
    public static User createUser() {
        return new User(null, "张三", "zhangsan@example.com");
    }
    
    public static User createUser(String username) {
        return new User(null, username, username.toLowerCase() + "@example.com");
    }
    
    public static List<User> createUsers(int count) {
        return IntStream.range(0, count)
            .mapToObj(i -> createUser("user" + i))
            .toList();
    }
}

class UserServiceTest {
    
    @Test
    void testCreate() {
        User user = TestDataFactory.createUser();
        
        when(userRepository.save(any())).thenReturn(user);
        
        User result = userService.create(user);
        
        assertThat(result).isNotNull();
    }
}

9. 小结

本章学习了 Spring Boot 测试的核心内容:

内容要点
单元测试JUnit 5、Mockito、AssertJ
集成测试@SpringBootTest、@DataJpaTest
Web 测试MockMvc、WebTestClient
安全测试@WithMockUser、@WithUserDetails
测试事务@Transactional、@Rollback
测试切片@DataJpaTest、@JsonTest
测试工具TestRestTemplate、WebTestClient

下一章将学习 Spring Boot 可观测性。