news 2026/2/9 18:42:26

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手把手教会你写单元测试 —— 从“不敢测”到“测得爽”(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% 覆盖但测了所有分支。

❌ “测试代码不用维护”

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

✅ 正确心态:

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


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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/9 15:20:01

LOOKUP函数典型用法合集

LOOKUP函数主要用于在查找范围中查询指定的查找值&#xff0c;并返回另一个范围中对应位置的值。 她有两个特点&#xff1a; 1、要求查询区域必须升序进行排序。如果没有经过排序&#xff0c;LOOKUP函数也会认为排在数据区域最后的内容&#xff0c;是该区域中最大的。 2、当…

作者头像 李华
网站建设 2026/2/4 6:11:38

Java程序员如何深入学习Spring源码?

金三银四也快要到了&#xff0c;不知道大家最近面试的时候有没有被问到过Spring相关问题&#xff08;循环依赖、事务、生命周期、传播特性、IOC、AOP、设计模式、源码&#xff09;&#xff1f;拿Spring来说&#xff0c;现在面试面试官一般会直接问&#xff1a;谈一下你对Spring…

作者头像 李华
网站建设 2026/2/7 14:15:11

HTTP 请求方法选择与 RESTful 实践(对比 GraphQL、RPC)

HTTP请求方法在实际开发中并非仅使用POST&#xff0c;但确实存在简化使用现象。 早期因技术限制&#xff08;如浏览器表单仅支持GET/POST&#xff09;和简化思维导致过度使用POST。 现代开发推荐RESTful风格&#xff1a;GET查询、POST创建、PUT/PATCH更新、DELETE删除&#xff…

作者头像 李华
网站建设 2026/2/8 11:28:40

AI 驱动人才管理落地难?Moka 全流程解决方案助力企业破局

在数字化转型浪潮下&#xff0c;企业对人力资源管理的效率与精准度要求不断提升&#xff0c;智慧人力信息系统逐渐成为企业管理的重要工具。很多 HR 从业者和企业管理者想了解智慧人力信息系统的具体定义与价值&#xff0c;也希望找到实现 AI 驱动全流程人才管理的有效路径。本…

作者头像 李华
网站建设 2026/2/5 4:46:32

便携式移动气象监测设备

便携式移动气象监测设备设计与实现 一、设计背景与意义 气象监测在农业生产、环境治理、科研勘探、应急救援等领域至关重要&#xff0c;传统气象监测设备体积庞大、依赖固定站点、部署成本高&#xff0c;难以满足移动观测与临时监测需求。现有便携气象设备多存在参数测量单一…

作者头像 李华
网站建设 2026/2/8 19:22:35

便携式信号发生器

便携式信号发生器设计与实现 一、设计背景与意义 信号发生器作为电子测量、电路调试、教学实验的核心工具&#xff0c;广泛应用于电子工程、通信技术、科研实验等领域。传统台式信号发生器存在体积庞大、依赖市电、操作复杂等问题&#xff0c;难以满足户外现场调试、移动设备维…

作者头像 李华