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

相关推荐
张同学的IT技术日记几秒前
必看!用示例代码学 C++ 基础入门,快速掌握基础知识,高效提升编程能力
后端
林太白9 分钟前
Nuxt3 功能篇
前端·javascript·后端
得物技术33 分钟前
营销会场预览直通车实践|得物技术
后端·架构·测试
Ice__Cai1 小时前
Flask 入门详解:从零开始构建 Web 应用
后端·python·flask·数据类型
武子康1 小时前
大数据-74 Kafka 核心机制揭秘:副本同步、控制器选举与可靠性保障
大数据·后端·kafka
紫穹1 小时前
006.LangChain Prompt Template
后端
whitepure1 小时前
万字详解JavaObject类方法
java·后端
切克呦1 小时前
通过 Cursor CLI 使用 GPT-5 的教程
前端·后端·程序员
Ice__Cai1 小时前
Flask 之 Request 对象详解:全面掌握请求数据处理
后端·python·flask·request·python web框架
gitboyzcf1 小时前
Git 常用命令
前端·git·后端