大家好呀! 今天我要和大家聊聊Spring Boot测试的那些事儿。作为一名Java开发者,写代码很重要,但写测试同样重要! 想象一下,你建了一座漂亮的房子,但如果没有质量检查,你敢住进去吗?测试就是我们的"质量检查员"!今天重点介绍两个超级明星:@SpringBootTest和Mockito,保证让你学得明明白白!
先讲个小故事:小明写了一个计算器程序,能算加减乘除。他自信满满地交给老师,结果老师输入"5÷0",程序直接崩溃了!如果有测试,这种问题早就能发现啦!
测试的好处多多:
Spring Boot提供了一整套测试工具:
今天我们先重点聊聊@SpringBootTest和Mockito这对黄金搭档!✨
@SpringBootTest就像是给你的Spring Boot应用做全身检查⚕️。它会启动几乎整个应用上下文,包括所有的bean、配置、数据库连接等等。
@SpringBootTest
class MyApplicationTests {
@Autowired
private MyService myService; // 可以自动注入真实的bean
@Test
void contextLoads() {
assertThat(myService).isNotNull();
}
}
@SpringBootTest有三种启动模式,就像汽车的档位:
MOCK(默认):模拟Servlet环境,不启动真实服务器
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
RANDOM_PORT:启动真实服务器,随机端口
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
DEFINED_PORT:使用application.properties中定义的端口
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
NONE:不提供任何Servlet环境
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
假设我们有个用户服务:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
}
测试这个服务:
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
void shouldGetUserById() {
// 准备测试数据
User testUser = new User(1L, "张三");
userRepository.save(testUser);
// 执行测试
User result = userService.getUserById(1L);
// 验证结果
assertThat(result.getName()).isEqualTo("张三");
}
@Test
void shouldThrowExceptionWhenUserNotFound() {
assertThatThrownBy(() -> userService.getUserById(999L))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("User not found");
}
}
Mockito就像是电影里的替身演员,它可以:
为什么需要Mockito?因为单元测试应该独立!我们不希望测试UserService时,真的去调用数据库或第三方API。
// 创建一个模拟的UserRepository
UserRepository mockRepo = Mockito.mock(UserRepository.class);
或者使用注解更简洁:
@Mock
private UserRepository userRepository;
@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this); // 初始化@Mock注解
}
// 当调用findById(1L)时,返回预设的用户
Mockito.when(userRepository.findById(1L))
.thenReturn(Optional.of(new User(1L, "张三")));
// 当调用findById(999L)时,返回空
Mockito.when(userRepository.findById(999L))
.thenReturn(Optional.empty());
// 验证findById(1L)被调用了一次
Mockito.verify(userRepository, Mockito.times(1))
.findById(1L);
// 验证deleteById从未被调用
Mockito.verify(userRepository, Mockito.never())
.deleteById(Mockito.anyLong());
使用@MockBean替换Spring上下文中的真实bean:
@SpringBootTest
class UserServiceMockTest {
@Autowired
private UserService userService; // 真实服务
@MockBean
private UserRepository userRepository; // 模拟仓库
@Test
void shouldGetUserByIdWithMock() {
// 设置模拟行为
Mockito.when(userRepository.findById(1L))
.thenReturn(Optional.of(new User(1L, "张三")));
// 调用真实服务方法
User result = userService.getUserById(1L);
// 验证
assertThat(result.getName()).isEqualTo("张三");
Mockito.verify(userRepository).findById(1L);
}
}
// 任何Long类型的ID
Mockito.when(userRepository.findById(Mockito.anyLong()))
.thenReturn(Optional.of(new User(1L, "默认用户")));
// 特定条件的参数
Mockito.when(userRepository.findByName(Mockito.argThat(name -> name.length() > 5)))
.thenReturn(Optional.of(new User(1L, "长名字用户")));
Mockito.when(userRepository.save(Mockito.any()))
.thenThrow(new RuntimeException("数据库错误"));
Mockito.when(userRepository.count())
.thenReturn(10L) // 第一次调用返回10
.thenReturn(20L) // 第二次返回20
.thenThrow(new RuntimeException("太多调用")); // 第三次抛出异常
InOrder inOrder = Mockito.inOrder(userRepository);
// 验证先调用findById,再调用save
inOrder.verify(userRepository).findById(1L);
inOrder.verify(userRepository).save(Mockito.any(User.class));
@SpringBootTest启动整个应用,确实会比较慢。解决方案:
@SpringBootTest
+ @DirtiesContext
测试时操作数据库要注意:
@Transactional
@Rollback // 默认就是true
@Test
void testWithDatabase() { ... }
@AfterEach
void tearDown() {
userRepository.deleteAll();
}
对于外部API调用:
测试Controller层️:
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
Mockito.when(userService.getUserById(1L))
.thenReturn(new User(1L, "张三"));
mockMvc.perform(MockMvcRequestBuilders.get("/users/1"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("张三"));
}
}
健康的测试套件应该像金字塔️:
测试类型 | 适用场景 | 推荐工具 |
---|---|---|
纯业务逻辑 | Service层核心逻辑 | JUnit + Mockito |
数据库交互 | Repository层 | @DataJpaTest + TestEntityManager |
Web层 | Controller | @WebMvcTest + MockMvc |
完整流程 | 应用启动到API调用 | @SpringBootTest + TestRestTemplate |
客户端交互 | 前端调用API | @SpringBootTest + WebTestClient |
好的测试名应该像说明书:
[方法名]_[状态]_[预期]
例如:
@Test
void getUserById_withInvalidId_shouldThrowException() { ... }
@Test
void saveUser_withValidUser_shouldReturnSavedUser() { ... }
让我们通过一个完整的用户管理系统示例来实践:
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
// getters/setters
}
public interface UserRepository extends JpaRepository {
Optional findByEmail(String email);
}
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
public User registerUser(String name, String email) {
if (userRepository.findByEmail(email).isPresent()) {
throw new EmailAlreadyExistsException("Email already registered");
}
User user = new User();
user.setName(name);
user.setEmail(email);
return userRepository.save(user);
}
public User getUserByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
}
class UserServiceUnitTest {
private UserService userService;
@Mock
private UserRepository userRepository;
@BeforeEach
void setUp() {
userService = new UserService(userRepository);
}
@Test
void registerUser_withNewEmail_shouldSaveUser() {
// 准备
String name = "张三";
String email = "[email protected]";
// 模拟findByEmail返回空
Mockito.when(userRepository.findByEmail(email))
.thenReturn(Optional.empty());
// 模拟save返回用户
User savedUser = new User(1L, name, email);
Mockito.when(userRepository.save(Mockito.any(User.class)))
.thenReturn(savedUser);
// 执行
User result = userService.registerUser(name, email);
// 验证
assertThat(result.getId()).isNotNull();
assertThat(result.getEmail()).isEqualTo(email);
// 验证交互
Mockito.verify(userRepository).findByEmail(email);
Mockito.verify(userRepository).save(Mockito.any(User.class));
}
@Test
void registerUser_withExistingEmail_shouldThrowException() {
String email = "[email protected]";
// 模拟已存在用户
Mockito.when(userRepository.findByEmail(email))
.thenReturn(Optional.of(new User()));
// 执行并验证异常
assertThatThrownBy(() -> userService.registerUser("任何名字", email))
.isInstanceOf(EmailAlreadyExistsException.class)
.hasMessageContaining("Email already registered");
}
}
@DataJpaTest
class UserRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void findByEmail_whenUserExists_shouldReturnUser() {
// 保存测试用户
User user = new User(null, "李四", "[email protected]");
entityManager.persist(user);
entityManager.flush();
// 查询
Optional found = userRepository.findByEmail(user.getEmail());
// 验证
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("李四");
}
@Test
void findByEmail_whenUserNotExists_shouldReturnEmpty() {
Optional found = userRepository.findByEmail("[email protected]");
assertThat(found).isEmpty();
}
}
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserSystemIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@AfterEach
void tearDown() {
userRepository.deleteAll();
}
@Test
void fullUserRegistrationFlow_shouldWork() {
// 准备注册请求
Map request = new HashMap<>();
request.put("name", "王五");
request.put("email", "[email protected]");
// 调用注册API
ResponseEntity response = restTemplate.postForEntity(
"http://localhost:" + port + "/api/users",
request,
User.class);
// 验证响应
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getId()).isNotNull();
assertThat(response.getBody().getEmail()).isEqualTo("[email protected]");
// 验证数据库
Optional dbUser = userRepository.findByEmail("[email protected]");
assertThat(dbUser).isPresent();
// 调用查询API
ResponseEntity getResponse = restTemplate.getForEntity(
"http://localhost:" + port + "/api/[email protected]",
User.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().getName()).isEqualTo("王五");
}
}
测试覆盖率是衡量测试完整性的重要指标:
由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
HTTP、HTTPS、Cookie 和 Session 之间的关系
什么是 Cookie?简单介绍与使用方法
什么是 Session?如何应用?
使用 Spring 框架构建 MVC 应用程序:初学者教程
有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
如何理解应用 Java 多线程与并发编程?
把握Java泛型的艺术:协变、逆变与不可变性一网打尽
Java Spring 中常用的 @PostConstruct 注解使用总结
如何理解线程安全这个概念?
理解 Java 桥接方法
Spring 整合嵌入式 Tomcat 容器
Tomcat 如何加载 SpringMVC 组件
“在什么情况下类需要实现 Serializable,什么情况下又不需要(一)?”
“避免序列化灾难:掌握实现 Serializable 的真相!(二)”
如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)
解密 Redis:如何通过 IO 多路复用征服高并发挑战!
线程 vs 虚拟线程:深入理解及区别
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
“打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”
Java 中消除 If-else 技巧总结
线程池的核心参数配置(仅供参考)
【人工智能】聊聊Transformer,深度学习的一股清流(13)
Java 枚举的几个常用技巧,你可以试着用用
由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
HTTP、HTTPS、Cookie 和 Session 之间的关系
使用 Spring 框架构建 MVC 应用程序:初学者教程
有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
Java Spring 中常用的 @PostConstruct 注解使用总结
线程 vs 虚拟线程:深入理解及区别
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
探索 Lombok 的 @Builder 和 @SuperBuilder:避坑指南(一)
为什么用了 @Builder 反而报错?深入理解 Lombok 的“暗坑”与解决方案(二)