单元测试编写规范总结
单元测试编写规范适用版本Spring Boot 2.2.13JUnit 5.6.3Mockito 3.6.28Java 8作用域全模块 Java 单元测试编写标准本文参考业内大佬总结1.黑白盒测试—2.Java 全链路测试体系1. 总则本规范适用于常见 java 项目模块的纯单元测试编写目标是统一风格所有模块的测试代码结构一致任何开发者都能快速读懂。高可维护性测试即文档用例名称本身就是测试场景的描述。高可信度通过严格的隔离策略和异常覆盖确保测试真正验证了业务逻辑。1.1 技术栈约束组件版本说明JUnit Jupiter5.6.3测试框架Java 8 兼容的最后稳定版Mockito3.6.28Mock 框架Java 8 兼容的最后稳定版AssertJ3.18.1流式断言库可选推荐Spring Boot Test2.2.13.RELEASE仅集成测试使用单元测试禁止启动 Spring为什么锁定 Java 8 兼容版本本项目基于 JDK 1.8Mockito 5.x 和 JUnit 5.11 均要求 Java 11强行使用会导致编译/运行时报错。2. 测试分类与 Tag 规范2.1 自定义 Tag 注解强制所有测试类必须使用项目统一定义的 Tag 注解禁止裸写Tag(xxx)。可在公共依赖模块的 xxxx.test 包下预定义注解// 纯单元测试仅使用 Mockito无任何外部中间件依赖Target(ElementType.TYPE)Retention(RetentionPolicy.RUNTIME)Tag(unit)publicinterfaceUnitTest{}// 集成测试依赖 DB / Redis / MQ / HTTP 接口等外部中间件Target(ElementType.TYPE)Retention(RetentionPolicy.RUNTIME)Tag(integration)publicinterfaceIntegrationTest{}2.2 分类判定标准场景分类判定依据工具类AESUtil、StringUtilsUnitTest纯计算无任何外部依赖Service 方法Mock 掉 MapperUnitTest仅依赖 MapperMock 后无真实 DB 交互Service → Mapper 真实读写IntegrationTest需要连接数据库MinIO / Redis / MQTT 交互IntegrationTest需要连接外部中间件Controller HTTP 契约验证IntegrationTest需要启动 Spring 上下文2.3 使用示例importcommon.test.UnitTest;// ✅ 正确使用自定义注解UnitTestExtendWith(MockitoExtension.class)DisplayName(AES 工具类单元测试)classAesTest{...}// ❌ 错误裸写 TagTag(unit)ExtendWith(MockitoExtension.class)classAesTest{...}3. 命名规范3.1 测试类命名类型命名格式示例单元测试被测类名 TestSysUserServiceImplTest、AESUtilTest集成测试被测类名 ITSysConfigServiceImplIT接口测试被测Controller ApiTestSysUserControllerApiTest3.2 测试方法命名强制格式被测方法名_输入条件_预期结果采用驼峰命名三段之间用下划线_分隔。方法名含义getUserById_NullUserId_ThrowsIllegalArgumentException传入 null 用户ID预期抛出非法参数异常setUserPassword_EmptyPassword_ThrowsServiceException密码为空预期抛出业务异常selectConfigByKey_ValidKey_ReturnsTypedValue合法 key预期返回类型正确的值encrypt_ValidInput_ReturnsBase64Cipher合法输入加密预期返回 Base64 密文list_NullParam_ReturnsEmptyList参数为 null预期返回空列表反面示例禁止// ❌ 含义不明Testvoidtest1(){...}// ❌ 无法看出输入和预期TestvoidtestGetUser(){...}// ❌ 缺少输入条件段TestvoidgetUserById_ReturnsUser(){...}3.3 DisplayName强制每个测试方法必须添加DisplayName用中文自然语言描述测试场景TestDisplayName(用户ID为null时抛出IllegalArgumentException)voidgetUserById_NullUserId_ThrowsIllegalArgumentException(){...}4. AAA 模式强制每个测试方法必须严格遵循Arrange → Act → Assert三段式结构三段之间用空行分隔。4.1 结构说明阶段职责包含内容Arrange准备构造测试数据、配置 Mock 行为when(...).thenReturn(...)、对象构建Act执行调用被测方法一行核心调用Assert断言验证结果assertXxx、verify4.2 完整示例UnitTestExtendWith(MockitoExtension.class)DisplayName(用户服务单元测试)classSysUserServiceImplTest{MockprivateSysUserMapperuserMapper;InjectMocksprivateSysUserServiceImpluserService;TestDisplayName(根据合法ID查询用户返回对应用户信息)voidgetById_ValidUserId_ReturnsUser(){// Arrange SysUserexpectedUsernewSysUser();expectedUser.setUserId(1L);expectedUser.setUserName(zhangsan);when(userMapper.selectById(1L)).thenReturn(expectedUser);// Act SysUseractualUseruserService.getById(1L);// Assert assertThat(actualUser).isNotNull();assertThat(actualUser.getUserId()).isEqualTo(1L);assertThat(actualUser.getUserName()).isEqualTo(zhangsan);verify(userMapper).selectById(1L);}}4.3 AAA 各段禁忌禁忌说明Arrange 中调用被测方法准备阶段不应触发被测逻辑Act 段包含断言执行和验证必须分离Assert 段构造新数据断言阶段只验证不准备多个 Act 调用一个用例只测一个行为路径5. Mock 规范5.1 只 Mock 直接依赖核心原则Mock 的边界是被测类的直接外部依赖不要穿透到依赖的依赖。// ✅ 正确UserService 直接依赖 UserMapper只 Mock MapperMockprivateSysUserMapperuserMapper;InjectMocksprivateSysUserServiceImpluserService;// ❌ 错误穿透 Mock 了 Mapper 依赖的 DataSource / SqlSessionMockprivateDataSourcedataSource;MockprivateSqlSessionsqlSession;判断标准如果被测类通过Autowired直接注入了某个 Bean就 Mock 它如果是间接依赖被注入的 Bean 内部使用的不要 Mock。5.2 Mock 行为配置规范// ✅ 精确匹配when(userMapper.selectById(1L)).thenReturn(expectedUser);// ✅ 参数匹配器需要灵活匹配时when(userMapper.selectById(anyLong())).thenReturn(expectedUser);// ✅ Mock 异常行为when(userMapper.selectById(anyLong())).thenThrow(newRuntimeException(DB error));// ❌ 禁止在 when 中执行真实业务逻辑when(userMapper.selectById(anyLong())).thenCallRealMethod();// 单元测试中禁止5.3 verify 使用规范// ✅ 验证交互次数verify(userMapper,times(1)).selectById(1L);// ✅ 验证从未调用verify(userMapper,never()).deleteById(anyLong());// ✅ 验证无更多交互verifyNoMoreInteractions(userMapper);原则verify用于验证行为发生了assert用于验证结果正确了。两者互补不要混用。6. 断言规范6.1 优先使用 AssertJ 流式断言AssertJ 提供可读性更强的链式断言推荐在单元测试中使用// ✅ 推荐AssertJ 流式断言assertThat(user).isNotNull();assertThat(user.getUserName()).isEqualTo(zhangsan);assertThat(userList).hasSize(3).extracting(SysUser::getUserName).contains(zhangsan);// ✅ 可接受JUnit 5 原生断言assertNotNull(user);assertEquals(zhangsan,user.getUserName());// ❌ 禁止JUnit 4 断言org.junit.AssertAssert.assertNotNull(user);// 必须迁移到 JUnit 56.2 断言必须明确// ✅ 明确断言assertThat(result.getCode()).isEqualTo(200);assertThat(result.getData()).isNotNull();assertThat(result.getData().getUserId()).isEqualTo(1L);// ❌ 模糊断言太弱无法发现问题assertNotNull(result);6.3 常用断言速查// 空值判断assertThat(result).isNull();assertThat(result).isNotNull();// 集合判断assertThat(list).isEmpty();assertThat(list).hasSize(3);assertThat(list).containsExactly(item1,item2,item3);// 字符串判断assertThat(str).startsWith(prefix);assertThat(str).contains(keyword);// 数值判断assertThat(count).isGreaterThan(0);assertThat(score).isBetween(0.0,100.0);// 异常断言assertThatThrownBy(()-service.doSomething(null)).isInstanceOf(ServiceException.class).hasMessageContaining(不能为空);7. 异常场景覆盖强制所有公开方法的异常路径必须有用例覆盖。这是 bug 的高发区。7.1 必须覆盖的异常类型异常类型说明示例参数校验异常入参为 null、空字符串、非法值setUserPassword(null)→ServiceException业务异常违反业务规则删除已有角色的用户 →ServiceException边界条件空集合、最大长度、零值、负数selectById(-1L)外部依赖异常Mock 的依赖抛出异常Mapper 抛RuntimeException7.2 异常测试示例TestDisplayName(密码为空时抛出ServiceException)voidsetUserPassword_EmptyPassword_ThrowsServiceException(){// Arrange SysUserusernewSysUser();user.setPassword();// Act Assert assertThatThrownBy(()-userService.setUserPassword(user)).isInstanceOf(ServiceException.class).hasMessageContaining(密码不能为空);}TestDisplayName(用户对象为null时抛出ServiceException)voidsetUserPassword_NullUser_ThrowsServiceException(){// Act Assert assertThatThrownBy(()-userService.setUserPassword(null)).isInstanceOf(ServiceException.class).hasMessageContaining(密码不能为空);}TestDisplayName(Mapper查询异常时异常向上传播)voidgetById_MapperThrowsException_PropagatesException(){// Arrange when(userMapper.selectById(anyLong())).thenThrow(newRuntimeException(数据库连接失败));// Act Assert assertThatThrownBy(()-userService.getById(1L)).isInstanceOf(RuntimeException.class).hasMessageContaining(数据库连接失败);}8. 不测试私有方法8.1 原则私有方法是类的内部实现细节不应直接测试。如果某个私有方法复杂到需要独立测试说明该类职责过大应进行重构将私有方法提取到一个新的工具类/服务类中将其改为public方法为新类编写独立的单元测试8.2 重构示例// ❌ 重构前私有方法难以测试publicclassDeviceService{publicDeviceStatusparseStatus(StringrawData){StringnormalizednormalizeData(rawData);// 私有方法returnconvertToStatus(normalized);// 私有方法}privateStringnormalizeData(Stringraw){...}// 逻辑复杂需要测试privateDeviceStatusconvertToStatus(Stringdata){...}}// ✅ 重构后拆分为独立类publicclassDeviceService{privatefinalDeviceDataParserdataParser;publicDeviceService(DeviceDataParserdataParser){this.dataParserdataParser;}publicDeviceStatusparseStatus(StringrawData){StringnormalizeddataParser.normalizeData(rawData);returndataParser.convertToStatus(normalized);}}// 新类可独立测试publicclassDeviceDataParser{publicStringnormalizeData(Stringraw){...}publicDeviceStatusconvertToStatus(Stringdata){...}}9. 测试隔离与生命周期9.1 测试独立性每个测试方法必须独立运行不依赖其他测试的执行顺序或共享状态。// ✅ 正确每个测试方法独立构造数据TestvoidmethodA_ValidInput_ReturnsResult(){SysUserusernewSysUser();// 独立构造user.setUserId(1L);// ...}TestvoidmethodB_ValidInput_ReturnsResult(){SysUserusernewSysUser();// 独立构造不复用 methodA 的数据user.setUserId(2L);// ...}9.2 BeforeEach / AfterEach 使用仅在所有测试方法共享相同初始化/清理逻辑时使用BeforeEachvoidsetUp(){// 所有测试方法共享的 Mock 配置when(userMapper.selectById(anyLong())).thenReturn(buildDefaultUser());}AfterEachvoidtearDown(){// 清理资源如果有}9.3 禁止事项禁止行为原因测试方法之间共享可变状态执行顺序不确定导致结果不稳定在测试中访问文件系统/网络破坏隔离性使用 Mock 替代在测试中 sleep 等待使用Awaitility或重构为可测试的设计硬编码环境相关配置使用ActiveProfiles(test)或 Mock10. 落地检查清单每个测试类提交前对照以下清单自检类级别标注UnitTest或IntegrationTest禁止裸写Tag类级别标注DisplayName(中文描述)每个方法命名格式为方法名_条件_预期结果每个方法标注DisplayName(中文描述)遵循 AAA 模式三段之间有空行分隔只 Mock 了被测类的直接依赖正常路径、参数校验异常、业务异常、边界条件均有覆盖未测试私有方法如需测试则重构拆分断言明确不使用assertNotNull一笔带过无硬编码环境配置IP、端口、密码等11. CI 执行命令# 只跑单元测试mvntest-Dgroupsunit# 只跑集成测试mvn verify-Dgroupsintegration# 跑全部测试mvntest# 跳过测试快速打包mvn package-DskipTests

相关新闻

ruoyi-vue初步接触

ruoyi-vue初步接触

ruoyi-vue初步接触 引言:从零开始认识ruoyi-vue对于刚接触Java全栈开发的同学来说,“ruoyi-vue”这个名字或许有些陌生。实际上,它是一个基于Spring Boot、Spring Security、MyBatis和Vue.js的开源后台管理系统框架。通过它,我们可…

2026/9/22 4:26:22 阅读更多 →
别把鸡蛋放一个板块:用 Python 计算持仓相关性 + 集中度预警,把风险关进笼子里

别把鸡蛋放一个板块:用 Python 计算持仓相关性 + 集中度预警,把风险关进笼子里

本文为量化风控方法研究,所有价格与收益序列均为示意版(模拟数据),不构成任何投资建议。代码可直接运行,用于学习"相关性监控 集中度预警"的思路,非实盘交易依据,不涉及任何真实策略…

2026/9/22 4:26:34 阅读更多 →
AlphaFold技术解析:从蛋白质结构预测到生物医学革命

AlphaFold技术解析:从蛋白质结构预测到生物医学革命

1. AlphaFold技术革命全景回顾 2018年那个潮湿的伦敦夏天,DeepMind团队在蛋白质结构预测竞赛CASP13上扔下了一枚"AI炸弹"。当时我在结构生物学实验室亲眼目睹同事们传阅预测结果时脸上难以置信的表情——那些蓝色和绿色的带状图与实验测得的真实结构几乎完…

2026/9/22 3:19:18 阅读更多 →

最新新闻

5个坑教你搞懂后端安全保障措施源码避坑指南

5个坑教你搞懂后端安全保障措施源码避坑指南

5个坑教你搞懂后端安全保障措施源码避坑指南 配置环境就卡半天?别急着骂娘。很多时候不是你的网络慢,也不是Docker没配好,而是你根本没看懂框架底层那些 安全保障措施 是怎么拦截你的请求的。今天这篇 避坑指南…

2026/9/22 5:04:15 阅读更多 →
钓鱼发烧友攻略:3步搞定实战项目搭建

钓鱼发烧友攻略:3步搞定实战项目搭建

钓鱼发烧友攻略:3步搞定实战项目搭建 刚啃完Python或JS语法书,面对空白编辑器发呆?这是90%初学者的死穴。 学会语法却不知怎么搭项目 ,是技术成长的第一道坎。别慌,咱们不背八股文,直接上手。…

2026/9/22 5:04:15 阅读更多 →
巧影去水印最佳实践:告别报错与黑盒的3步实战

巧影去水印最佳实践:告别报错与黑盒的3步实战

巧影去水印最佳实践:告别报错与黑盒的3步实战 报错一堆看不懂?StackTrace 满屏飘?很多刚入行的开发者在面对“巧影去水印”这类具体需求时,第一反应往往是去搜现成的脚本,结果一运行,Python 报错…

2026/9/22 5:04:15 阅读更多 →
3步搞定仙逆下载,从入门到精通避坑指南

3步搞定仙逆下载,从入门到精通避坑指南

3步搞定仙逆下载,从入门到精通避坑指南 很多刚转行做开发的朋友,盯着屏幕上的代码发呆,明明语法都背熟了,一动手搭项目就卡壳。这种“会写代码却不会造轮子”的窘境,是每个从入门到精通路上必须跨过的坎。别慌,今天咱们不聊虚的,直接拿“仙逆下载”这…

2026/9/22 5:04:14 阅读更多 →
卓越亚马逊购书网实战:3个避坑指南助你搞定版本升级

卓越亚马逊购书网实战:3个避坑指南助你搞定版本升级

卓越亚马逊购书网实战:3个避坑指南助你搞定版本升级 版本升级后 API 全变了,这种崩溃感只有写过老项目的人才懂。别慌,这篇 避坑指南 专为中小施工企业负责人定制,带你用运维开发视角拆解卓越亚马逊购书网背后的技术逻辑。…

2026/9/22 5:04:14 阅读更多 →
公主救王子开发指南:前端老手带你啃透版本升级API变更的保姆级教程

公主救王子开发指南:前端老手带你啃透版本升级API变更的保姆级教程

公主救王子开发指南:前端老手带你啃透版本升级API变更的保姆级教程 版本号一升级,接口全炸了?别慌,这就是典型的“公主救王子”式重构现场。很多刚毕业的朋友拿到旧项目,看着满屏红色的报错,心里慌得一批。其实这就是典型的 版本升级后 API…

2026/9/22 5:03:14 阅读更多 →

日新闻

3台商务办公笔记本实测:手写实现环境配置,告别卡半天

3台商务办公笔记本实测:手写实现环境配置,告别卡半天

3台商务办公笔记本实测:手写实现环境配置,告别卡半天 配置环境就卡半天?别怪机器慢,多半是你没选对工具链。在Java、Go或Python的项目现场, 手写实现…

2026/9/22 0:00:41 阅读更多 →
剑帝加点速查手册:3分钟搞懂核心逻辑

剑帝加点速查手册:3分钟搞懂核心逻辑

剑帝加点速查手册:3分钟搞懂核心逻辑 面试被问原理答不上来,是不是常态?别慌。很多开发者对着 GitHub 开源仓库里的代码发呆,看似简单实则暗藏玄机。今天这份【剑帝加点】速查手册,直接带你拆解核心实现,把面试必考的原理讲透。…

2026/9/22 0:00:41 阅读更多 →
手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优 复制来的代码跑不通不知道怎么调?别慌,这种“复制粘贴地狱”在开发圈太常见了。尤其是做 图片压缩网站…

2026/9/22 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/22 4:32:41 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/22 4:38:57 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/21 4:51:05 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/21 15:36:51 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/21 15:36:51 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/22 2:43:42 阅读更多 →