单元测试指南
本文档基于项目实际代码,展示单元测试的编写规范和最佳实践。
📁 测试文件位置
blog-common/src/test/java/
└── com/blog/common/security/
└── JwtTokenProviderTest.java # JWT 工具类测试
blog-modules/blog-module-system/blog-system-service/src/test/java/
└── com/blog/system/service/impl/
├── UserServiceImplTest.java # 用户服务测试
└── RemoteUserServiceImplTest.java # 远程用户服务测试
blog-modules/blog-module-file/blog-file-service/src/test/java/
└── com/blog/file/
└── BitifulStorageUnitTest.java # 对象存储测试
🎯 命名规范
遵循项目规范 should_expectedBehavior_when_state():
// ✅ 推荐
void should_ReturnUserVo_When_RegisterSuccess()
void should_ThrowException_When_InvalidCredentials()
void should_UpdateUser_When_UpdateByDto()
// ❌ 避免
void testRegister()
void registerTest()
📝 实战示例
JwtTokenProvider 测试
@Slf4j
@ExtendWith(MockitoExtension.class)
class JwtTokenProviderTest {
@Mock
private SecurityProperties securityProperties;
@InjectMocks
private JwtTokenProvider jwtTokenProvider;
@BeforeEach
void setUp() {
// 使用 lenient() 避免 UnnecessaryStubbingException
lenient().when(securityProperties.getJwtSecret())
.thenReturn("test-secret-key-for-testing-purposes-only");
lenient().when(securityProperties.getJwtExpiration())
.thenReturn(7200000L);
}
@Test
@DisplayName("should_GenerateValidToken_When_UserDetailsProvided")
void should_GenerateValidToken_When_UserDetailsProvided() {
log.info("Testing: Token Generation with Valid UserDetails");
// Given
UserDetails userDetails = User.builder()
.username("testuser")
.password("password")
.authorities("ROLE_USER")
.build();
Long userId = 100L;
// When
String token = jwtTokenProvider.generateToken(userDetails, userId);
log.info("When: Generated token: {}", token.substring(0, 50) + "...");
// Then
assertNotNull(token);
assertTrue(token.startsWith("eyJ"));
log.info("Then: Token generated successfully.");
}
}
UserServiceImpl 测试
@Slf4j
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@Mock
private UserMapper userMapper;
@Mock
private RoleMapper roleMapper;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private JwtTokenProvider jwtTokenProvider;
@Mock
private UserConverter userConverter;
@InjectMocks
private UserServiceImpl userService;
@BeforeEach
void setUp() {
// 注入 @Value 属性
ReflectionTestUtils.setField(userService, "jwtExpiration", 7200000L);
// 注入 MyBatis-Plus 父类 baseMapper
ReflectionTestUtils.setField(userService, "baseMapper", userMapper);
}
@Test
@DisplayName("should_ReturnUserVo_When_RegisterSuccess")
void should_ReturnUserVo_When_RegisterSuccess() {
log.info("Testing: User Registration - Success Scenario");
// Given
RegisterDTO registerDTO = new RegisterDTO();
registerDTO.setUsername("newuser");
registerDTO.setPassword("password123");
registerDTO.setEmail("newuser@example.com");
registerDTO.setNickname("New User");
SysUser mockUser = new SysUser();
mockUser.setId(100L);
mockUser.setUsername("newuser");
SysRole mockRole = new SysRole();
mockRole.setId(1L);
mockRole.setRoleKey("USER");
// Mock 行为
when(userMapper.selectOne(any())).thenReturn(null); // 用户不存在
when(passwordEncoder.encode(any())).thenReturn("encodedPassword");
when(roleMapper.selectOne(any())).thenReturn(mockRole);
UserVO mockUserVO = new UserVO();
mockUserVO.setId(100L);
mockUserVO.setUsername("newuser");
when(userConverter.entityToVo(any())).thenReturn(mockUserVO);
// When
UserVO result = userService.register(registerDTO);
log.info("When: userService.register called, result user ID: {}", result.getId());
// Then
assertNotNull(result);
assertEquals(100L, result.getId());
verify(userMapper).insert(any(SysUser.class));
log.info("Then: Registration flow verified successfully.");
}
@Test
@DisplayName("should_ThrowBusinessException_When_UsernameExists")
void should_ThrowBusinessException_When_UsernameExists() {
log.info("Testing: User Registration - Username Exists Scenario");
// Given
RegisterDTO registerDTO = new RegisterDTO();
registerDTO.setUsername("existingUser");
SysUser existingUser = new SysUser();
existingUser.setId(99L);
when(userMapper.selectOne(any())).thenReturn(existingUser);
// When & Then
assertThrows(BusinessException.class, () -> {
userService.register(registerDTO);
});
verify(userMapper, never()).insert(any(SysUser.class));
log.info("Then: BusinessException thrown as expected.");
}
}
⚙️ 处理特殊场景
MyBatis-Plus 继承问题
当测试继承自 BaseServiceImpl 的 Service 时:
@BeforeEach
void setUp() {
// 注入 MyBatis-Plus 的 baseMapper 字段
ReflectionTestUtils.setField(userService, "baseMapper", userMapper);
}
@Value 属性注入
@BeforeEach
void setUp() {
// 注入 @Value 配置属性
ReflectionTestUtils.setField(userService, "jwtExpiration", 7200000L);
}
Mockito Strictness
当遇到 UnnecessaryStubbingException:
// 方式 1: 在 setUp 中使用 lenient()
lenient().when(mock.method()).thenReturn(value);
// 方式 2: 在测试方法上添加注解
@Test
@MockitoSettings(strictness = Strictness.LENIENT)
void should_UpdateUser_When_UpdateByDto() { ... }
🚀 运行测试
# 运行单个测试类
mvn test -Dtest=UserServiceImplTest
# 运行多个测试类
mvn test -Dtest=JwtTokenProviderTest,UserServiceImplTest
# 运行带覆盖率报告
# 在 IntelliJ IDEA 中: Run 'Tests' with Coverage
📊 当前覆盖率
| 模块 | 类覆盖率 | 方法覆盖率 | 行覆盖率 |
|---|---|---|---|
com.blog.system.service.impl | 100% | 33.3% | 7.2% |
com.blog.common.security | 50% | 9.1% | 2.2% |
提示: 使用 Mock 可以隔离测试,但实际代码覆盖率可能较低。建议结合集成测试提升覆盖率。