大家好呀!👋 今天我要和大家聊聊Spring Boot测试的那些事儿。作为一名Java开发者,写代码很重要,但写测试同样重要!💯 想象一下,你建了一座漂亮的房子🏠,但如果没有质量检查,你敢住进去吗?测试就是我们的"质量检查员"!今天重点介绍两个超级明星:@SpringBootTest和Mockito,保证让你学得明明白白!😊
📚 第一章:为什么要写测试?测试有多重要?
1.1 测试的重要性
先讲个小故事📖:小明写了一个计算器程序,能算加减乘除。他自信满满地交给老师,结果老师输入"5÷0",程序直接崩溃了💥!如果有测试,这种问题早就能发现啦!
测试的好处多多:
- 🛡️ 防错保护:提前发现bug,避免上线后出问题
- 🔄 安全重构:改代码时不担心破坏原有功能
- 📝 文档作用:测试用例本身就是最好的使用示例
- � 持续集成:自动化测试是CI/CD的基础
1.2 Spring Boot测试生态
Spring Boot提供了一整套测试工具:
- 🧪 @SpringBootTest:完整应用上下文测试
- 🎭 Mockito:模拟对象和行为
- 🏗️ TestEntityManager:数据库测试
- 🌐 WebTestClient:WebFlux测试
- 🖥️ MockMvc:Web MVC测试
今天我们先重点聊聊@SpringBootTest和Mockito这对黄金搭档!✨
🚀 第二章:@SpringBootTest - 你的应用完整测试工具
2.1 什么是@SpringBootTest?
@SpringBootTest就像是给你的Spring Boot应用做全身检查👨⚕️。它会启动几乎整个应用上下文,包括所有的bean、配置、数据库连接等等。
@SpringBootTest
class MyApplicationTests {@Autowiredprivate MyService myService; // 可以自动注入真实的bean@Testvoid contextLoads() {assertThat(myService).isNotNull();}
}
2.2 @SpringBootTest的几种模式
@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)
2.3 实际应用示例
假设我们有个用户服务:
@Service
public class UserService {@Autowiredprivate UserRepository userRepository;public User getUserById(Long id) {return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException("User not found"));}
}
测试这个服务:
@SpringBootTest
class UserServiceTest {@Autowiredprivate UserService userService;@Autowiredprivate UserRepository userRepository;@Testvoid shouldGetUserById() {// 准备测试数据User testUser = new User(1L, "张三");userRepository.save(testUser);// 执行测试User result = userService.getUserById(1L);// 验证结果assertThat(result.getName()).isEqualTo("张三");}@Testvoid shouldThrowExceptionWhenUserNotFound() {assertThatThrownBy(() -> userService.getUserById(999L)).isInstanceOf(UserNotFoundException.class).hasMessageContaining("User not found");}
}
2.4 最佳实践
- 合理选择web环境:根据测试需求选择MOCK、RANDOM_PORT或NONE
- 使用@TestConfiguration:覆盖特定bean而不是全部配置
- 注意测试顺序:使用@DirtiesContext确保测试独立性
- 控制测试范围:不要过度使用@SpringBootTest,轻量级测试更高效
🎭 第三章:Mockito - 测试中的"替身演员"
3.1 什么是Mockito?
Mockito就像是电影里的替身演员🎬,它可以:
- 创建虚拟对象(模拟对象)
- 定义这些对象的行为
- 验证它们是否被正确调用
为什么需要Mockito?因为单元测试应该独立!我们不希望测试UserService时,真的去调用数据库或第三方API。
3.2 Mockito核心功能
3.2.1 创建Mock对象
// 创建一个模拟的UserRepository
UserRepository mockRepo = Mockito.mock(UserRepository.class);
或者使用注解更简洁:
@Mock
private UserRepository userRepository;@BeforeEach
void setup() {MockitoAnnotations.openMocks(this); // 初始化@Mock注解
}
3.2.2 定义Mock行为
// 当调用findById(1L)时,返回预设的用户
Mockito.when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "张三")));// 当调用findById(999L)时,返回空
Mockito.when(userRepository.findById(999L)).thenReturn(Optional.empty());
3.2.3 验证交互
// 验证findById(1L)被调用了一次
Mockito.verify(userRepository, Mockito.times(1)).findById(1L);// 验证deleteById从未被调用
Mockito.verify(userRepository, Mockito.never()).deleteById(Mockito.anyLong());
3.3 Mockito与@SpringBootTest结合
使用@MockBean替换Spring上下文中的真实bean:
@SpringBootTest
class UserServiceMockTest {@Autowiredprivate UserService userService; // 真实服务@MockBeanprivate UserRepository userRepository; // 模拟仓库@Testvoid 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);}
}
3.4 Mockito高级技巧
3.4.1 参数匹配器
// 任何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, "长名字用户")));
3.4.2 抛出异常
Mockito.when(userRepository.save(Mockito.any())).thenThrow(new RuntimeException("数据库错误"));
3.4.3 连续调用不同结果
Mockito.when(userRepository.count()).thenReturn(10L) // 第一次调用返回10.thenReturn(20L) // 第二次返回20.thenThrow(new RuntimeException("太多调用")); // 第三次抛出异常
3.4.4 验证调用顺序
InOrder inOrder = Mockito.inOrder(userRepository);// 验证先调用findById,再调用save
inOrder.verify(userRepository).findById(1L);
inOrder.verify(userRepository).save(Mockito.any(User.class));
3.5 Mockito最佳实践
- 不要过度Mock:只Mock必要的依赖
- 验证适度:不要过度验证内部实现
- 保持简单:每个测试只关注一个行为
- 使用@Spy谨慎:@Spy是部分Mock,可能导致测试不稳定
🔧 第四章:测试中的常见问题与解决方案
4.1 测试速度慢怎么办?
@SpringBootTest启动整个应用,确实会比较慢🐢。解决方案:
- 使用**@DataJpaTest**、@WebMvcTest等切片测试
- 合理使用**@MockBean**减少真实bean加载
- 共享测试上下文:
@SpringBootTest
+@DirtiesContext
4.2 数据库测试问题
测试时操作数据库📊要注意:
- 使用嵌入式数据库(H2)加速测试
- 每个测试后清理数据:
@Transactional @Rollback // 默认就是true @Test void testWithDatabase() { ... }
- 或者手动清理:
@AfterEach void tearDown() {userRepository.deleteAll(); }
4.3 第三方服务依赖
对于外部API调用🌐:
- 使用Mockito模拟
- 使用WireMock模拟HTTP服务
- 考虑契约测试(Pact)
4.4 测试Spring MVC
测试Controller层🖥️:
@WebMvcTest(UserController.class)
class UserControllerTest {@Autowiredprivate MockMvc mockMvc;@MockBeanprivate UserService userService;@Testvoid 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("张三"));}
}
🏆 第五章:测试金字塔与策略
5.1 测试金字塔
健康的测试套件应该像金字塔🏛️:
- 单元测试(70%):测试单个类,快速执行
- 集成测试(20%):测试组件间交互
- 端到端测试(10%):测试完整应用流程
5.2 如何选择测试工具
测试类型 | 适用场景 | 推荐工具 |
---|---|---|
纯业务逻辑 | Service层核心逻辑 | JUnit + Mockito |
数据库交互 | Repository层 | @DataJpaTest + TestEntityManager |
Web层 | Controller | @WebMvcTest + MockMvc |
完整流程 | 应用启动到API调用 | @SpringBootTest + TestRestTemplate |
客户端交互 | 前端调用API | @SpringBootTest + WebTestClient |
5.3 测试命名规范
好的测试名应该像说明书📝:
- 方法名说明测试场景和预期
- 常用模式:
[方法名]_[状态]_[预期]
例如:
@Test
void getUserById_withInvalidId_shouldThrowException() { ... }@Test
void saveUser_withValidUser_shouldReturnSavedUser() { ... }
🛠️ 第六章:实战演练 - 完整测试示例
让我们通过一个完整的用户管理系统示例来实践:
6.1 领域模型
@Entity
public class User {@Id @GeneratedValueprivate Long id;private String name;private String email;// getters/setters
}public interface UserRepository extends JpaRepository {Optional findByEmail(String email);
}
6.2 服务层
@Service
@Transactional
public class UserService {@Autowiredprivate 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"));}
}
6.3 完整的测试套件
6.3.1 单元测试(Service层)
class UserServiceUnitTest {private UserService userService;@Mockprivate UserRepository userRepository;@BeforeEachvoid setUp() {userService = new UserService(userRepository);}@Testvoid registerUser_withNewEmail_shouldSaveUser() {// 准备String name = "张三";String email = "zhangsan@example.com";// 模拟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));}@Testvoid registerUser_withExistingEmail_shouldThrowException() {String email = "exists@example.com";// 模拟已存在用户Mockito.when(userRepository.findByEmail(email)).thenReturn(Optional.of(new User()));// 执行并验证异常assertThatThrownBy(() -> userService.registerUser("任何名字", email)).isInstanceOf(EmailAlreadyExistsException.class).hasMessageContaining("Email already registered");}
}
6.3.2 集成测试(Repository层)
@DataJpaTest
class UserRepositoryIntegrationTest {@Autowiredprivate TestEntityManager entityManager;@Autowiredprivate UserRepository userRepository;@Testvoid findByEmail_whenUserExists_shouldReturnUser() {// 保存测试用户User user = new User(null, "李四", "lisi@example.com");entityManager.persist(user);entityManager.flush();// 查询Optional found = userRepository.findByEmail(user.getEmail());// 验证assertThat(found).isPresent();assertThat(found.get().getName()).isEqualTo("李四");}@Testvoid findByEmail_whenUserNotExists_shouldReturnEmpty() {Optional found = userRepository.findByEmail("nonexistent@example.com");assertThat(found).isEmpty();}
}
6.3.3 完整集成测试
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserSystemIntegrationTest {@LocalServerPortprivate int port;@Autowiredprivate TestRestTemplate restTemplate;@Autowiredprivate UserRepository userRepository;@AfterEachvoid tearDown() {userRepository.deleteAll();}@Testvoid fullUserRegistrationFlow_shouldWork() {// 准备注册请求Map request = new HashMap<>();request.put("name", "王五");request.put("email", "wangwu@example.com");// 调用注册APIResponseEntity 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("wangwu@example.com");// 验证数据库Optional dbUser = userRepository.findByEmail("wangwu@example.com");assertThat(dbUser).isPresent();// 调用查询APIResponseEntity getResponse = restTemplate.getForEntity("http://localhost:" + port + "/api/users?email=wangwu@example.com",User.class);assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);assertThat(getResponse.getBody().getName()).isEqualTo("王五");}
}
🎯 第七章:测试覆盖率与质量门禁
7.1 测试覆盖率
测试覆盖率是衡量测试完整性的重要指标📊:
- 行覆盖率:执行了多少百分比代码行
- 分支覆盖率:是否覆盖了所有if-else分支
- 变异测试:更高级的覆盖率分析
推荐阅读文章
-
由 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 的“暗坑”与解决方案(二)