SpringBoot单元测试全面解析
1. SpringBoot单元测试是什么?
SpringBoot单元测试是针对SpringBoot应用程序中的单个组件(如类、方法)进行的独立测试。它使用专门的测试框架来验证代码的正确性,通常不依赖外部资源如数据库、网络服务等。
2. 单元测试的作用
- 验证代码正确性:确保每个单元按预期工作
- 早期发现问题:在开发阶段发现bug
- 支持重构:保证重构后功能不变
- 文档作用:作为代码使用示例
- 设计改进:促使编写可测试的、松耦合的代码
3. 为什么需要写单元测试?
// 没有单元测试的问题示例
public class Calculator {
public int divide(int a, int b) {
return a / b; // 潜在的除零异常
}
}
// 有单元测试可以提前发现问题
@Test
void testDivide_ByZero_ShouldThrowException() {
Calculator calculator = new Calculator();
assertThrows(ArithmeticException.class, () -> calculator.divide(10, 0));
}
4. 单元测试的标准写法
AAA模式(Arrange-Act-Assert)
@Test
void testMethod_Scenario_ExpectedResult() {
// Arrange - 准备测试数据和环境
MyService service = new MyService();
String input = "test";
// Act - 执行被测试的方法
String result = service.process(input);
// Assert - 验证结果
assertEquals("PROCESSED_TEST", result);
}
5. 单元测试的目标
- 快速执行:测试应该快速运行
- 独立隔离:测试之间不相互依赖
- 可重复:每次运行结果一致
- 自验证:测试自动判断通过或失败
- 及时编写:在开发代码时同步编写
6. 私有方法测试建议
不推荐直接测试私有方法,因为:
- 私有方法是实现细节,可能经常变化
- 应该通过公共接口测试私有方法的功能
- 如果私有方法很复杂,考虑提取到单独的类中
7. 单元测试疑难杂点
- 外部依赖:使用Mock解决
- 数据库操作:使用内存数据库或Mock
- 静态方法:使用PowerMock或重构代码
- 时间相关:使用固定时间或Clock类
- 并发问题:需要专门的并发测试
8. 常用单元测试工具
| 工具 | 用途 |
|---|---|
| JUnit 5 | 测试框架 |
| Mockito | Mock框架 |
| AssertJ | 流式断言 |
| Testcontainers | 容器化测试 |
| @SpringBootTest | 集成测试 |
9. 重要概念
- Test Double:测试替身(Dummy, Fake, Stub, Mock, Spy)
- Fixture:测试固定装置
- Test Coverage:测试覆盖率
- TDD:测试驱动开发
- BDD:行为驱动开发
10. 综合性单元测试示例
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.*;
/**
* 用户服务单元测试示例
* 演示完整的单元测试编写方法
*/
@ExtendWith(MockitoExtension.class) // 使用Mockito扩展
class UserServiceTest {
@Mock
private UserRepository userRepository; // 模拟用户仓库
@Mock
private EmailService emailService; // 模拟邮件服务
@InjectMocks
private UserService userService; // 被测试的服务,自动注入模拟依赖
private User testUser;
private User adminUser;
/**
* 每个测试方法执行前的准备工作
*/
@BeforeEach
void setUp() {
// 准备测试数据
testUser = User.builder()
.id(1L)
.username("testuser")
.email("test@example.com")
.password("encodedPassword")
.status(UserStatus.ACTIVE)
.createdAt(LocalDateTime.now())
.build();
adminUser = User.builder()
.id(2L)
.username("admin")
.email("admin@example.com")
.password("adminPassword")
.role(UserRole.ADMIN)
.status(UserStatus.ACTIVE)
.createdAt(LocalDateTime.now())
.build();
}
/**
* 测试成功查找用户场景
*/
@Test
@DisplayName("根据ID查找用户 - 用户存在时应返回用户")
void findById_WhenUserExists_ShouldReturnUser() {
// Arrange - 准备测试数据和行为
given(userRepository.findById(1L)).willReturn(Optional.of(testUser));
// Act - 执行被测试的方法
Optional<User> result = userService.findById(1L);
// Assert - 验证结果
assertThat(result).isPresent();
assertThat(result.get().getUsername()).isEqualTo("testuser");
assertThat(result.get().getEmail()).isEqualTo("test@example.com");
// 验证模拟对象的交互
verify(userRepository, times(1)).findById(1L);
}
/**
* 测试用户不存在场景
*/
@Test
@DisplayName("根据ID查找用户 - 用户不存在时应返回空")
void findById_WhenUserNotExists_ShouldReturnEmpty() {
// Arrange
given(userRepository.findById(anyLong())).willReturn(Optional.empty());
// Act
Optional<User> result = userService.findById(999L);
// Assert
assertThat(result).isEmpty();
verify(userRepository).findById(999L);
}
/**
* 测试创建用户成功场景
*/
@Test
@DisplayName("创建用户 - 有效用户数据时应成功创建并发送欢迎邮件")
void createUser_WithValidData_ShouldCreateUserAndSendEmail() {
// Arrange
UserCreateRequest request = new UserCreateRequest("newuser", "new@example.com", "password123");
User savedUser = User.builder()
.id(3L)
.username("newuser")
.email("new@example.com")
.password("encodedPassword")
.status(UserStatus.ACTIVE)
.build();
given(userRepository.existsByUsername("newuser")).willReturn(false);
given(userRepository.existsByEmail("new@example.com")).willReturn(false);
given(userRepository.save(any(User.class))).willReturn(savedUser);
doNothing().when(emailService).sendWelcomeEmail(anyString(), anyString());
// Act
User result = userService.createUser(request);
// Assert
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(3L);
assertThat(result.getUsername()).isEqualTo("newuser");
assertThat(result.getStatus()).isEqualTo(UserStatus.ACTIVE);
// 验证正确的交互顺序和次数
verify(userRepository).existsByUsername("newuser");
verify(userRepository).existsByEmail("new@example.com");
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("new@example.com", "newuser");
}
/**
* 测试创建用户时用户名已存在场景
*/
@Test
@DisplayName("创建用户 - 用户名已存在时应抛出异常")
void createUser_WhenUsernameExists_ShouldThrowException() {
// Arrange
UserCreateRequest request = new UserCreateRequest("existinguser", "new@example.com", "password123");
given(userRepository.existsByUsername("existinguser")).willReturn(true);
// Act & Assert
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(DuplicateUserException.class)
.hasMessage("用户名已存在: existinguser");
// 验证保存方法没有被调用
verify(userRepository, never()).save(any(User.class));
verify(emailService, never()).sendWelcomeEmail(anyString(), anyString());
}
/**
* 测试批量操作用户
*/
@Test
@DisplayName("批量激活用户 - 应成功激活所有用户")
void activateUsers_WithUserList_ShouldActivateAllUsers() {
// Arrange
List<Long> userIds = Arrays.asList(1L, 2L, 3L);
List<User> users = Arrays.asList(testUser, adminUser);
given(userRepository.findAllById(userIds)).willReturn(users);
given(userRepository.saveAll(anyList())).willReturn(users);
// Act
List<User> result = userService.activateUsers(userIds);
// Assert
assertThat(result).hasSize(2);
assertThat(result).allMatch(user -> user.getStatus() == UserStatus.ACTIVE);
verify(userRepository).findAllById(userIds);
verify(userRepository).saveAll(anyList());
}
/**
* 测试边界情况 - 空输入
*/
@Test
@DisplayName("查找用户 - 空ID时应抛出异常")
void findById_WithNullId_ShouldThrowException() {
// Act & Assert
assertThatThrownBy(() -> userService.findById(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("用户ID不能为空");
}
/**
* 测试性能相关 - 超时测试
*/
@Test
@DisplayName("处理用户数据 - 应在指定时间内完成")
void processUserData_ShouldCompleteWithinTimeout() {
// 这个测试会验证方法执行不超过2秒
assertTimeoutPreemptively(java.time.Duration.ofSeconds(2), () -> {
userService.processUserData(testUser);
});
}
/**
* 测试异常处理
*/
@Test
@DisplayName("删除用户 - 用户不存在时应抛出异常")
void deleteUser_WhenUserNotExists_ShouldThrowException() {
// Arrange
given(userRepository.findById(999L)).willReturn(Optional.empty());
// Act & Assert
assertThatThrownBy(() -> userService.deleteUser(999L))
.isInstanceOf(UserNotFoundException.class)
.hasMessage("用户不存在: 999");
}
/**
* 使用BDD风格测试(行为驱动开发)
*/
@Test
@DisplayName("BDD风格 - 用户登录成功")
void givenValidCredentials_whenLogin_thenReturnUser() {
// Given - 准备阶段
String username = "testuser";
String password = "password123";
given(userRepository.findByUsernameAndPassword(username, password))
.willReturn(Optional.of(testUser));
// When - 执行阶段
Optional<User> result = userService.login(username, password);
// Then - 验证阶段
assertThat(result).isPresent();
assertThat(result.get().getUsername()).isEqualTo(username);
}
}
// 支持类定义
class User {
// 用户实体类
private Long id;
private String username;
private String email;
private String password;
private UserStatus status;
private UserRole role;
private LocalDateTime createdAt;
// builder模式
public static UserBuilder builder() { return new UserBuilder(); }
// getters and setters
}
class UserBuilder {
private User user = new User();
public UserBuilder id(Long id) { /* ... */ return this; }
public UserBuilder username(String username) { /* ... */ return this; }
public User build() { return user; }
}
enum UserStatus { ACTIVE, INACTIVE, SUSPENDED }
enum UserRole { USER, ADMIN }
class UserCreateRequest {
private String username;
private String email;
private String password;
// constructor, getters
}
// 自定义异常类
class DuplicateUserException extends RuntimeException {
public DuplicateUserException(String message) { super(message); }
}
class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) { super(message); }
}
关键要点总结
- 测试命名规范:方法名_场景_预期结果
- 单一职责:每个测试只测试一个场景
- 使用Mock:隔离外部依赖
- 全面覆盖:包括正常、异常、边界情况
- 及时维护:随着代码变更更新测试
- 避免过度测试:关注业务逻辑,而不是实现细节
这个示例展示了SpringBoot单元测试的最佳实践,包括测试结构、Mock使用、异常测试、边界条件测试等,可以作为实际项目中的参考模板。
1272

被折叠的 条评论
为什么被折叠?



