markdown
# Spring Boot单元测试:Mockito+JUnit5全覆盖策略

## 引言
在持续交付的敏捷开发中,**单元测试覆盖率**是衡量代码质量的核心指标。Spring Boot项目如何通过`JUnit5`+`Mockito`实现**100%测试覆盖率**?本文将手把手教你构建分层测试体系,揭秘单元测试中的**6大避坑指南**,并给出企业级实战方案!
---
## 一、环境搭建与基础配置
### 1.1 依赖引入(Maven)
```xml
org.junit.jupiter
junit-jupiter
5.8.2
test
org.mockito
mockito-inline
4.5.1
test
org.springframework.boot
spring-boot-starter-test
test
java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void getUserById_WhenExist_ReturnUser() {
// Given
User mockUser = new User(1L, "test");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// When
User result = userService.getUserById(1L);
// Then
assertEquals("test", result.getUsername());
verify(userRepository, times(1)).findById(1L);
}
}
java
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void getUser_ShouldReturn200() throws Exception {
when(userService.getUserById(1L))
.thenReturn(new User(1L, "mock_user"));
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("mock_user"));
}
}
java
// 模糊匹配任意参数
when(userRepository.save(any(User.class)))
.thenReturn(new User(1L, "saved_user"));
// 自定义参数校验
verify(userService).updateUser(argThat(user ->
user.getUsername().length() > 5));
java
@Test
void transferMoney_WhenBalanceInsufficient_ThrowException() {
when(accountService.getBalance(anyLong()))
.thenReturn(100.0);
assertThrows(InsufficientBalanceException.class, () -> {
transferService.transfer(1L, 2L, 200.0);
});
}
java
try (MockedStatic utilities = mockStatic(AuthUtils.class)) {
utilities.when(AuthUtils::getCurrentUserId).thenReturn(100L);
assertEquals(100L, userService.getCurrentUserId());
}
java
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
class UserRepositoryTest {
@Container
static PostgreSQLContainer> postgres =
new PostgreSQLContainer<>("postgres:13");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
}
@Test
void saveUser_ShouldPersistData() {
User user = repository.save(new User(null, "container_user"));
assertNotNull(user.getId());
}
}
xml
org.jacoco
jacoco-maven-plugin
0.8.7
prepare-agent
report
BUNDLE
LINE
COVEREDRATIO
0.95
https://img-blog.csdnimg.cn/direct/9a1b3c4d4e5b4f7c9c3d4a5b0e8d4e4c.png
过度Mock陷阱
❌错误做法:Mock所有依赖类
✅正确方案:仅Mock外部依赖(数据库、API等),保持业务逻辑真实执行
测试顺序依赖
java
@TestMethodOrder(MethodOrderer.Random.class) // 强制随机执行顺序
class OrderSensitiveTest { ... }
忽略时间敏感测试
java
@Test
void scheduleTask_ShouldExecuteWithin5s() {
assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
scheduler.executeTask();
});
}
循环依赖导致Mock失效
解决方案:使用@MockBean
替代@Autowired
静态方法Mock内存泄漏
java
// 使用try-with-resources确保资源释放
try (MockedStatic mocked = mockStatic(Utility.class)) {
// ...
}
忽略异常分支测试
java
@Test
void divide_WhenDivisorIsZero_ThrowException() {
assertThrows(ArithmeticException.class,
() -> calculator.divide(10, 0));
}
测试金字塔模型
https://img-blog.csdnimg.cn/direct/8a1d3c4d4a5a4d6c9b3e7c8d9f4e4b4a.png
单元测试(70%) > 集成测试(20%) > 端到端测试(10%)
测试命名规范
java
// 格式:[Method]_[State]_[Result]
@Test
void getUserById_WhenIdIsNegative_ThrowIllegalArgument() { ... }
持续集成集成覆盖率检查
yaml
# GitHub Actions示例
- name: Verify coverage
run: mvn verify -Djacoco.check=true
通过本文的Mockito+JUnit5组合拳,某金融系统成功将单元测试覆盖率从58%提升至97%,缺陷率下降76%。记住:不要为了覆盖率而写测试,要为质量而写!
技术拓展:
《Spring Boot集成测试全攻略》
《Mockito深度解析》
#SpringBoot# #单元测试# #JUnit5# #Mockito# 更多干货,关注作者获取最新技术动态!