第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应用上下文测试
@WebMvcTest
Web层测试,只加载MVC相关组件
@DataJpaTest
JPA数据层测试,使用内存数据库
@JsonTest
JSON序列化和反序列化测试
@TestConfiguration
测试专用配置类
@MockBean
Spring上下文中的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 🏠 返回课程首页