Spring Boot Web 服务单元测试设计指南

在 Spring Boot Web 项目中,单元测试应聚焦单个组件的验证,隔离外部依赖(如数据库、网络服务)。以下是分层测试策略和最佳实践:

一、核心测试框架组合
XML 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
二、分层单元测试策略

Service 层测试

使用 Mockito 模拟 Repository 依赖

java 复制代码
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;

    @Test
    void getUserById_WhenExists_ReturnsUser() {
        // 1. 准备 Mock 数据
        User mockUser = new User(1L, "test@example.com");
        when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));

        // 2. 执行测试
        User result = userService.getUserById(1L);

        // 3. 验证结果
        assertThat(result.getEmail()).isEqualTo("test@example.com");
        verify(userRepository).findById(1L); // 验证调用
    }

    @Test
    void getUserById_WhenNotExists_ThrowsException() {
        when(userRepository.findById(anyLong())).thenReturn(Optional.empty());
        
        assertThatThrownBy(() -> userService.getUserById(99L))
            .isInstanceOf(UserNotFoundException.class);
    }
}

Controller 层测试

使用 MockMvc 模拟 HTTP 请求

java 复制代码
@WebMvcTest(UserController.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;

    @Test
    void getUser_ValidId_Returns200() throws Exception {
        User mockUser = new User(1L, "test@example.com");
        when(userService.getUserById(1L)).thenReturn(mockUser);

        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.email").value("test@example.com"));
    }

    @Test
    void createUser_InvalidInput_Returns400() throws Exception {
        String invalidJson = "{ \"email\": \"bad-email\" }";
        
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(invalidJson))
            .andExpect(status().isBadRequest());
    }
}

Repository 层测试
注意: 真实数据库交互属于集成测试。单元测试可使用内存数据库:

java 复制代码
@DataJpaTest
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;

    @Test
    void findByEmail_WhenExists_ReturnsUser() {
        // 1. 准备数据
        User savedUser = entityManager.persistFlushFind(
            new User(null, "test@example.com"));
        
        // 2. 执行查询
        Optional<User> result = userRepository.findByEmail("test@example.com");
        
        // 3. 验证结果
        assertThat(result).isPresent();
        assertThat(result.get().getId()).isNotNull();
    }
}
三、关键测试场景设计
层级 测试场景 验证要点
Service 业务逻辑分支 异常处理/事务边界/条件覆盖
Controller 请求验证 HTTP状态码/响应体/错误处理
Util 纯逻辑组件 算法正确性/边界条件
四、最佳实践
  1. 命名规范
    被测方法_测试条件_预期结果 模式
java 复制代码
@Test
void transferFunds_InsufficientBalance_ThrowsException() { ... }

测试隔离

使用 @BeforeEach 重置测试状态:

java 复制代码
@BeforeEach
void setup() {
    reset(mockDependency); // 重置Mock状态
}

验证异常

JUnit 5 异常断言:

java 复制代码
assertThrows(ValidationException.class, 
    () -> service.processRequest(invalidRequest));

参数化测试

覆盖多组输入:

java 复制代码
@ParameterizedTest
@ValueSource(strings = {"2023-01-01", "2023-13-01", "invalid-date"})
void parseDate_InvalidInput_ThrowsException(String input) {
    assertThrows(DateTimeParseException.class, 
        () -> DateUtils.parse(input));
}
五、测试覆盖率优化
  • 使用 JaCoCo 检查覆盖率:
XML 复制代码
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.8</version>
  <executions>
      <execution>
          <goals>
              <goal>prepare-agent</goal>
              <goal>report</goal>
          </goals>
      </execution>
  </executions>
</plugin>
  • 推荐目标

    • 服务层:≥ 80%

    • 关键工具类:100%

    • Controller:验证核心状态码(非强制100%)

关键原则:单元测试应聚焦当前组件的责任,避免跨层验证。通过 Mock 隔离依赖,确保测试快速执行(单个测试类 < 1秒)。结合集成测试覆盖整体流程,形成完整的测试金字塔。

相关推荐
大鸡腿同学4 小时前
【成长类】《只有偏执狂才能生存》读书笔记:程序员的偏执型成长地图
后端
0xDevNull4 小时前
MySQL数据冷热分离详解
后端·mysql
AI袋鼠帝4 小时前
OpenClaw(龙虾)最强开源对手!Github 40K Star了,又一个爆火的Agent..
后端
KevinCyao4 小时前
java视频短信接口怎么调用?SpringBoot集成视频短信及回调处理Demo
java·spring boot·音视频
總鑽風5 小时前
搭建Spring Boot + ELK日志平台,实现可视化日志监控
spring boot·elk·macos
不吃香菜学java5 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
新知图书5 小时前
搭建Spring Boot开发环境
java·spring boot·后端
皮皮林5515 小时前
SpringBoot 4 最被低估的新特性:Spring Data AOT
spring boot
宸津-代码粉碎机6 小时前
Spring Boot 4.0虚拟线程实战调优技巧,最大化发挥并发优势
java·人工智能·spring boot·后端·python
MaCa .BaKa6 小时前
47-心里健康咨询平台/心理咨询系统
java·spring boot·mysql·tomcat·maven·intellij-idea·个人开发