1426 字
约 4 分钟
1
SpringBoot 测试
2026-09-19
无标签

第17章 SpringBoot 测试

掌握SpringBoot测试框架和最佳实践 💻 查看完整代码 - 在线IDE体验

🎯 学习目标

  • 测试基础:了解测试理论和原则
  • 单元测试:掌握JUnit和Mockito
  • 集成测试:SpringBoot测试注解
  • Web测试:MockMvc和TestRestTemplate
  • 数据测试:@DataJpaTest等切片测试
  • 测试策略:测试金字塔和最佳实践

🔧 核心概念

  • Unit Test:单元测试
  • Integration Test:集成测试
  • Mock:模拟对象
  • Stub:存根
  • Test Slice:测试切片
  • Test Context:测试上下文

📦 测试依赖配置

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

<!-- 包含以下测试框架 -->
<!-- JUnit 5 - 测试框架 -->
<!-- Mockito - Mock框架 -->
<!-- AssertJ - 断言库 -->
<!-- Hamcrest - 匹配器 -->
<!-- Spring Test - Spring测试支持 -->
<!-- Spring Boot Test - SpringBoot测试支持 -->

<!-- 额外测试依赖 -->
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <scope>test</scope>
</dependency>

🔄 测试执行流程

编写测试→配置环境→执行测试→验证结果→生成报告→持续集成

🏗️ 测试金字塔

测试策略分层

                    🌐 端到端测试 (E2E Tests) - 少量
                
                    🔗 集成测试 (Integration Tests) - 适量
                
                    🧪 单元测试 (Unit Tests) - 大量

🧪 单元测试示例

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    @DisplayName("根据ID查找用户 - 成功")
    void findById_Success() {
        // Given
        Long userId = 1L;
        User mockUser = new User(userId, "张三", "zhangsan@example.com");
        when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));

        // When
        User result = userService.findById(userId);

        // Then
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(userId);
        assertThat(result.getName()).isEqualTo("张三");
        verify(userRepository).findById(userId);
    }

    @Test
    @DisplayName("根据ID查找用户 - 用户不存在")
    void findById_UserNotFound() {
        // Given
        Long userId = 999L;
        when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // When & Then
        assertThatThrownBy(() -> userService.findById(userId))
            .isInstanceOf(UserNotFoundException.class)
            .hasMessage("用户不存在: " + userId);
    }

    @Test
    @DisplayName("创建用户 - 成功")
    void createUser_Success() {
        // Given
        CreateUserRequest request = new CreateUserRequest("李四", "lisi@example.com");
        User savedUser = new User(2L, "李四", "lisi@example.com");
        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        // When
        User result = userService.createUser(request);

        // Then
        assertThat(result).isNotNull();
        assertThat(result.getName()).isEqualTo("李四");
        assertThat(result.getEmail()).isEqualTo("lisi@example.com");
    }
}

🌐 Web层测试

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    @DisplayName("获取用户信息 - 成功")
    void getUser_Success() throws Exception {
        // Given
        Long userId = 1L;
        User user = new User(userId, "张三", "zhangsan@example.com");
        when(userService.findById(userId)).thenReturn(user);

        // When & Then
        mockMvc.perform(get("/api/users/{id}", userId))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(userId))
            .andExpect(jsonPath("$.name").value("张三"))
            .andExpect(jsonPath("$.email").value("zhangsan@example.com"));
    }

    @Test
    @DisplayName("创建用户 - 成功")
    void createUser_Success() throws Exception {
        // Given
        CreateUserRequest request = new CreateUserRequest("李四", "lisi@example.com");
        User createdUser = new User(2L, "李四", "lisi@example.com");
        when(userService.createUser(any(CreateUserRequest.class))).thenReturn(createdUser);

        // When & Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.name").value("李四"))
            .andExpect(jsonPath("$.email").value("lisi@example.com"));
    }
}

🗄️ 数据层测试

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("根据邮箱查找用户")
    void findByEmail_Success() {
        // Given
        User user = new User("张三", "zhangsan@example.com");
        entityManager.persistAndFlush(user);

        // When
        Optional<User> found = userRepository.findByEmail("zhangsan@example.com");

        // Then
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("张三");
    }

    @Test
    @DisplayName("根据名称查找用户列表")
    void findByNameContaining_Success() {
        // Given
        entityManager.persistAndFlush(new User("张三", "zhangsan@example.com"));
        entityManager.persistAndFlush(new User("张四", "zhangsi@example.com"));
        entityManager.persistAndFlush(new User("李五", "liwu@example.com"));

        // When
        List<User> users = userRepository.findByNameContaining("张");

        // Then
        assertThat(users).hasSize(2);
        assertThat(users).extracting(User::getName)
            .containsExactlyInAnyOrder("张三", "张四");
    }
}

🔗 集成测试示例

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.properties")
class UserIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    @LocalServerPort
    private int port;

    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }

    @Test
    @DisplayName("用户完整流程测试")
    void userCompleteFlow() {
        // 1. 创建用户
        CreateUserRequest createRequest = new CreateUserRequest("张三", "zhangsan@example.com");
        
        ResponseEntity<User> createResponse = restTemplate.postForEntity(
            "/api/users", createRequest, User.class);
        
        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        User createdUser = createResponse.getBody();
        assertThat(createdUser).isNotNull();
        assertThat(createdUser.getName()).isEqualTo("张三");

        // 2. 查询用户
        ResponseEntity<User> getResponse = restTemplate.getForEntity(
            "/api/users/" + createdUser.getId(), User.class);
        
        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        User retrievedUser = getResponse.getBody();
        assertThat(retrievedUser).isNotNull();
        assertThat(retrievedUser.getId()).isEqualTo(createdUser.getId());

        // 3. 更新用户
        UpdateUserRequest updateRequest = new UpdateUserRequest("张三丰", "zhangsan@example.com");
        
        restTemplate.put("/api/users/" + createdUser.getId(), updateRequest);

        // 4. 验证更新
        ResponseEntity<User> updatedResponse = restTemplate.getForEntity(
            "/api/users/" + createdUser.getId(), User.class);
        
        User updatedUser = updatedResponse.getBody();
        assertThat(updatedUser.getName()).isEqualTo("张三丰");

        // 5. 删除用户
        restTemplate.delete("/api/users/" + createdUser.getId());

        // 6. 验证删除
        ResponseEntity<String> deletedResponse = restTemplate.getForEntity(
            "/api/users/" + createdUser.getId(), String.class);
        
        assertThat(deletedResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }
}

🏷️ 测试注解对比

@SpringBootTest完整的Spring Boot应用上下文测试@WebMvcTestWeb层测试,只加载MVC相关组件@DataJpaTestJPA数据层测试,使用内存数据库@JsonTestJSON序列化和反序列化测试@TestConfiguration测试专用配置类@MockBeanSpring上下文中的Mock Bean

🧪 测试类型分类

单元测试 (Unit Tests)集成测试 (Integration Tests)端到端测试 (E2E Tests)性能测试 (Performance Tests)安全测试 (Security Tests)

🐳 Testcontainers集成

@SpringBootTest
@Testcontainers
class DatabaseIntegrationTest {

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.datasource.username", mysql::getUsername);
        registry.add("spring.datasource.password", mysql::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("真实数据库环境测试")
    void testWithRealDatabase() {
        // Given
        User user = new User("张三", "zhangsan@example.com");
        
        // When
        User saved = userRepository.save(user);
        
        // Then
        assertThat(saved.getId()).isNotNull();
        assertThat(userRepository.findById(saved.getId())).isPresent();
    }
}

📊 测试覆盖率配置

<!-- Maven Surefire Plugin -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M7</version>
    <configuration>
        <includes>
            <include>**/*Test.java</include>
            <include>**/*Tests.java</include>
        </includes>
    </configuration>
</plugin>

<!-- JaCoCo Coverage Plugin -->
<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>

📊 测试框架对比

测试框架 优点 缺点 适用场景
JUnit 5 功能强大,注解丰富 学习成本较高 单元测试
Mockito Mock功能完善 过度使用影响测试质量 依赖隔离
TestContainers 真实环境测试 启动速度较慢 集成测试
WireMock HTTP服务模拟 配置复杂 外部服务测试
Selenium 真实浏览器测试 维护成本高 UI自动化测试

🎯 测试最佳实践

测试开发建议1. 测试命名:使用描述性的测试方法名2. AAA模式:Arrange-Act-Assert结构3. 独立性:测试之间不应相互依赖4. 快速反馈:单元测试应快速执行5. 覆盖率:追求有意义的测试覆盖6. 测试数据:使用测试专用数据7. 持续集成:自动化测试执行8. 测试维护:及时更新和重构测试

下一章节

🎉 恭喜完成第17章学习!

你已经掌握了SpringBoot测试的核心技能,接下来学习应用监控和管理! 🚀 下一章:SpringBoot Actuator 🏠 返回课程首页

SpringBoot 测试
http://www.clxhxhhr.top/posts/2885/
作者
clxstart
发布于
2026-09-19
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。