跳到主要内容

单元测试指南

本文档基于项目实际代码,展示单元测试的编写规范和最佳实践。

📁 测试文件位置

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.impl100%33.3%7.2%
com.blog.common.security50%9.1%2.2%

提示: 使用 Mock 可以隔离测试,但实际代码覆盖率可能较低。建议结合集成测试提升覆盖率。