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秒)。结合集成测试覆盖整体流程,形成完整的测试金字塔。

相关推荐
一 乐9 小时前
婚纱摄影网站|基于ssm + vue婚纱摄影网站系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot·后端
码事漫谈10 小时前
Protocol Buffers 编码原理深度解析
后端
码事漫谈10 小时前
gRPC源码剖析:高性能RPC的实现原理与工程实践
后端
期待のcode12 小时前
前后端分离项目 Springboot+vue 在云服务器上的部署
服务器·vue.js·spring boot
踏浪无痕12 小时前
AI 时代架构师如何有效成长?
人工智能·后端·架构
程序员小假12 小时前
我们来说一下无锁队列 Disruptor 的原理
java·后端
ProgramHan12 小时前
Spring Boot 3.2 新特性:虚拟线程的落地实践
java·jvm·spring boot
武子康13 小时前
大数据-209 深度理解逻辑回归(Logistic Regression)与梯度下降优化算法
大数据·后端·机器学习
maozexijr13 小时前
Rabbit MQ中@Exchange(durable = “true“) 和 @Queue(durable = “true“) 有什么区别
开发语言·后端·ruby
源码获取_wx:Fegn089514 小时前
基于 vue智慧养老院系统
开发语言·前端·javascript·vue.js·spring boot·后端·课程设计