在 Spring Boot Web 项目中,单元测试应聚焦单个组件的验证,隔离外部依赖(如数据库、网络服务)。以下是分层测试策略和最佳实践:
一、核心测试框架组合
<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 依赖
@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 请求
@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 层测试
注意: 真实数据库交互属于集成测试。单元测试可使用内存数据库:
@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 | 纯逻辑组件 | 算法正确性/边界条件 |
四、最佳实践
命名规范
被测方法_测试条件_预期结果
模式
@Test
void transferFunds_InsufficientBalance_ThrowsException() { ... }
测试隔离
使用 @BeforeEach
重置测试状态:
@BeforeEach
void setup() {
reset(mockDependency); // 重置Mock状态
}
验证异常
JUnit 5 异常断言:
assertThrows(ValidationException.class,
() -> service.processRequest(invalidRequest));
参数化测试
覆盖多组输入:
@ParameterizedTest
@ValueSource(strings = {"2023-01-01", "2023-13-01", "invalid-date"})
void parseDate_InvalidInput_ThrowsException(String input) {
assertThrows(DateTimeParseException.class,
() -> DateUtils.parse(input));
}
五、测试覆盖率优化
使用 JaCoCo 检查覆盖率:
<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秒)。结合集成测试覆盖整体流程,形成完整的测试金字塔。