当前位置: 首页 > news >正文

手把手教会你写单元测试 —— 从“不敢测”到“测得爽”(Spring Boot + JUnit 5 实战)

视频看了几百小时还迷糊?关注我,几分钟让你秒懂!(发点评论可以给博主加热度哦)


一、真实痛点:为什么你总在逃避写单元测试?

  • “业务太复杂,不知道怎么测!”
  • “写了测试,改代码又要改测试,太麻烦!”
  • “跑一次测试要连数据库、启动 Spring,慢得像蜗牛!”
  • “测了也没用,线上照样出 bug!”

🚨其实,你不是不会写测试,而是没掌握“正确的姿势”!

本文将通过真实业务场景 + 正反案例对比,手把手教你写出快、准、稳的单元测试!


二、什么是单元测试?一句话讲透

单元测试 = 对一个“最小可测试单元”(通常是方法)进行隔离验证,确保它在各种输入下都能正确工作。

✅ 核心原则:

  • :毫秒级执行,不依赖外部(DB、网络);
  • 隔离:只测目标方法,其他依赖全部 Mock;
  • 可重复:每次运行结果一致;
  • 全覆盖:正常流 + 异常流都要测。

三、反例警告:这些“伪测试”你一定写过!

❌ 反例 1:启动整个 Spring 容器(集成测试冒充单元测试)

@SpringBootTest // ←←← 错!这是集成测试! class UserServiceTest { @Test void testRegister() { // 连了数据库、Redis、MQ... // 跑一次 10 秒,谁受得了? } }

❌ 反例 2:只测 happy path(正常流程),不测异常

@Test void testAdd() { assertEquals(5, calculator.add(2, 3)); // 只测了正数 // 没测负数、零、溢出... }

❌ 反例 3:测试代码和业务代码强耦合

// 业务代码改了字段名,测试全红! assertEquals("张三", user.getName());

💥 这些都不是真正的单元测试!


四、手把手实战:用 JUnit 5 + Mockito 写纯单元测试

场景:用户注册服务(含手机号校验)

1️⃣ 业务代码(待测试)
@Service public class UserService { private final UserRepository userRepository; private final SmsService smsService; public UserService(UserRepository userRepository, SmsService smsService) { this.userRepository = userRepository; this.smsService = smsService; } public User register(String phone, String name) { if (userRepository.existsByPhone(phone)) { throw new IllegalArgumentException("手机号已存在"); } if (!isValidPhone(phone)) { throw new IllegalArgumentException("手机号格式错误"); } User user = new User(phone, name); userRepository.save(user); smsService.sendWelcomeSms(phone); // 发欢迎短信 return user; } private boolean isValidPhone(String phone) { return phone != null && phone.matches("^1[3-9]\\d{9}$"); } }

2️⃣ 单元测试(纯内存,0 依赖)
import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockitoExtension.class) // 启用 Mockito class UserServiceTest { @Mock private UserRepository userRepository; @Mock private SmsService smsService; @InjectMocks private UserService userService; // 自动注入 mock 依赖 @Test @DisplayName("注册成功:手机号合法且未注册") void shouldRegisterSuccessfully() { // Given(准备) String phone = "13800138000"; String name = "张三"; when(userRepository.existsByPhone(phone)).thenReturn(false); // When(执行) User result = userService.register(phone, name); // Then(断言) assertNotNull(result); assertEquals(phone, result.getPhone()); assertEquals(name, result.getName()); verify(userRepository).save(any(User.class)); // 验证 save 被调用 verify(smsService).sendWelcomeSms(phone); // 验证短信被发送 } @Test @DisplayName("注册失败:手机号已存在") void shouldThrowExceptionWhenPhoneExists() { // Given String phone = "13800138000"; when(userRepository.existsByPhone(phone)).thenReturn(true); // When & Then IllegalArgumentException ex = assertThrows( IllegalArgumentException.class, () -> userService.register(phone, "李四") ); assertEquals("手机号已存在", ex.getMessage()); verify(userRepository, never()).save(any()); // 确保没保存 verify(smsService, never()).sendWelcomeSms(any()); // 确保没发短信 } @Test @DisplayName("注册失败:手机号格式错误") void shouldThrowExceptionWhenPhoneInvalid() { // 测试多种非法手机号 assertInvalidPhone(null); assertInvalidPxone(""); assertInvalidPhone("123"); assertInvalidPhone("1380013800"); // 少一位 assertInvalidPhone("23800138000"); // 开头不是 1 } private void assertInvalidPhone(String phone) { IllegalArgumentException ex = assertThrows( IllegalArgumentException.class, () -> userService.register(phone, "王五") ); assertEquals("手机号格式错误", ex.getMessage()); } }

优势

  • 0 数据库:用@Mock模拟依赖;
  • 毫秒级运行:整个测试类 < 100ms;
  • 覆盖全面:成功 + 两种失败场景;
  • 验证行为:不仅看返回值,还验证savesendSms是否被正确调用。

五、关键工具介绍

注解/方法作用
@ExtendWith(MockitoExtension.class)启用 Mockito
@Mock创建 mock 对象(模拟依赖)
@InjectMocks创建被测对象,并自动注入 mock 依赖
when(...).thenReturn(...)定义 mock 行为
verify(...).method(...)验证方法是否被调用
assertThrows断言抛出异常
never()验证方法从未被调用

六、高级技巧:测试私有方法?别傻了!

很多新手问:“怎么测isValidPhone私有方法?”

正确答案:不要直接测私有方法!

  • 私有方法是实现细节,应该通过公有方法间接测试
  • 如果私有方法逻辑复杂,说明它该提取成独立工具类
// 提取成工具类(可单独测试) public class PhoneUtils { public static boolean isValid(String phone) { return phone != null && phone.matches("^1[3-9]\\d{9}$"); } } // 然后在 UserService 中调用 if (!PhoneUtils.isValid(phone)) { ... } // 单独测试 PhoneUtils class PhoneUtilsTest { @Test void testValidPhone() { ... } }

📌原则:测试行为,而不是实现


七、集成测试 vs 单元测试 —— 别再混淆!

类型注解速度用途
单元测试@ExtendWith(MockitoExtension.class)⚡ 毫秒级测单个类逻辑
集成测试@SpringBootTest🐌 几秒~几十秒测整个 Spring 上下文、DB 交互

建议比例

  • 单元测试:80%(快、稳、易维护)
  • 集成测试:20%(验证关键链路)

八、完整项目结构(Maven)

<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>

默认包含:

  • JUnit 5
  • Mockito
  • AssertJ
  • Hamcrest

九、最佳实践总结

  1. 命名清晰shouldDoXxxWhenYyy(如shouldThrowExceptionWhenPhoneExists
  2. 三段式结构:Given(准备)→ When(执行)→ Then(断言)
  3. 一个测试只测一个场景:不要在一个@Test里测多个逻辑
  4. Mock 外部依赖:DB、HTTP、MQ 全部模拟
  5. 覆盖边界条件:null、空、超长、特殊字符
  6. 不要测 getter/setter:除非有逻辑

十、常见误区

❌ “测试覆盖率越高越好”

错!100% 覆盖但没测到关键路径,不如 70% 覆盖但测了所有分支。

❌ “测试代码不用维护”

错!烂测试比没测试更可怕(给人虚假安全感)。

✅ 正确心态:

“写测试是为了让自己改代码时睡得着觉”


视频看了几百小时还迷糊?关注我,几分钟让你秒懂!(发点评论可以给博主加热度哦)

http://www.jsqmd.com/news/322053/

相关文章:

  • 2026松原工控产品:口碑厂家,助力高效生产,工控产品/施耐德电气/电气自动化/中低压电气,工控产品品牌有哪些
  • 西门子200smart系列化工反应釜程序探索
  • 国科大《ACS Omega》突破:1秒颠覆传统烧结!焦耳热冲击创制“双峰”晶粒SiC,晶粒尺寸激增60%
  • 第二届天文学与光学测量国际学术会议(AOM 2026
  • Java 数据结构你会用几种?—— 从“只会 ArrayList”到“精准选型”(附 Spring Boot 实战场景)
  • 【计算机毕业设计案例】基于springboot+vue的奶茶店线下点餐管理系统基于SpringBoot的奶茶店线上点单与库存管理系统设计与实现(程序+文档+讲解+定制)
  • 线下文档到 AI 知识库(AI-KB):企业内容数字化的新路径
  • 200smart 与威纶通触摸屏实现平面两轴直线插补(Ver1.2.7)
  • 《Nat. Commun.》:南昌大学/宁夏大学团队揭示脉冲加热动力学稳定机制,实现负载金属纳米颗粒的抗烧结锁定
  • 手把手教会你什么是 Spring 事件监听 —— 解耦神器,告别“面条代码”!(Spring Boot 实战)
  • 用户个人资料编辑功能:测试要点、陷阱与最佳实践
  • 机床排屑机厂家报价口碑推荐,选购、运维与售后关键要点分析
  • 2026年1月云真机、云手机、安卓云手机、苹果云手机、ios云手机厂家实力综合分析:天勤网络登顶
  • 订单提交手动测试流程指南
  • Claude Code提示词案例(开发联系我们页面,表单使用Element Plus的el-form组件实现)
  • 2026年 展位设计公司推荐排行榜:主题展位/品牌展位/特装展位/大小面积展位设计搭建,创意视觉与高效落地的专业服务商精选
  • 国内四大AI编程IDE对比(二):从零构建桌面应用实测(补上Trae,幸亏补上了)
  • ACPI!ACPIBuildProcessDeviceGenericEvalStrict函数和BuildRequest->CurrentObject的由来
  • 一个杀手级 prompt
  • Google Genie 3 技术架构拆解:世界模型如何为 Agent 训练提供无限环境
  • threoninamide;DFCYDWOTPTH-NH₂
  • 雨生红球藻主流品牌全景解析(2026年最新版)
  • 基于springboot的电影评价管理系统设计实现
  • 2026年 实验室气路系统厂家推荐排行榜:气体管路、集中供气、汇流排专业解决方案,安全高效精准供气
  • python的crypto模块no model解决方法
  • HTTP 请求方法选择与 RESTful 实践(对比 GraphQL、RPC)
  • mhpkg 文件使用方法
  • 微商企业未来迭代的核心方向与多元探索——以链动2+1模式AI智能名片商城小程序为核心支撑
  • 胡杨映碧水,金沙绕蓝湾,这是罗布湖独有的沙漠浪漫
  • 文件与文件夹批量更名工具FileReNameTool V2.1.1支持撤销改名