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

Junit5+Mockito实现已投票事件的测试策略

一.引言

1.业务场景

在某类活动报名中,邀请投票是一个很常见的功能,用户根据某个邀请进行投票,决定是否参与或选择某个选项,然而看似简单操作的背后却隐藏着层层业务约束,比如在我的项目中此投票规则只有已报报名的用户才能投票,且同一用户不能重复投票,一旦处理不当,轻则数据错乱,重则引发用户投诉。

以我们的vote(String id,String voter)方法为例,它至少需要应对以下测试验证:

1.参数校验:id(邀请ID)和voter(投票人)可能为null,空字符串或者空白字符串,测试的时候是否能够拒绝而不是抛出空指针异常(NullPointerException)

2.存在性校验:如果传入id在数据库里不存在,服务是否返回明确的业务异常(InvitationException)

3.资格校验:投票人必须位于findEnrollers(id)返回的报名表列表中,如果该方法返回null或者空集合,我们的逻辑是否能够处理。

4.幂等性校验: 如果findVote(id,voter)已返回true(表示已投票),业务必须拦截,防止重复计票

方案:本文将利用Junit5的参数化测试(@ParameterizedTest)和Mockito的Mock隔离,结合等价类划分——边界值设计高覆盖的测试用例

二.待测试代码展示

public void vote(String id, String voter) { // 1. 参数校验 if (id == null || id.isBlank()) { throw new InvitationException(400, "邀请ID不能为空"); } if (voter == null || voter.isBlank()) { throw new InvitationException(400, "投票人不能为空"); } // 2. 邀请必须存在 Invitation invitation = repository.findInvitationById(id); if (invitation == null) { throw new InvitationException(404, "邀请不存在"); } // 3. 只有已报名的用户才能投票 Set<String> enrollers = repository.findEnrollers(id); if (enrollers == null || !enrollers.contains(voter)) { throw new InvitationException(403, "该用户未报名此邀请,无法投票"); } // 4. 不能重复投票 if (repository.findVote(id, voter)) { throw new InvitationException(403, "已投票过"); } // 5. 持久化投票记录 repository.persistVote(id, voter); }

三.测试设计

(1)确定输入域。分析被测方法投票功能参数的所有属性及其约束条件

明确参数:id(邀请ID) ,voter(投票人)

明确外部依赖(需要Mock):findEnrollers(报名列表),findVote(是否已投)。

参数校验

输入条件

有效等价类

无效等价类

Id值

非空且存在的邀请ID

Null,空字符串“”,空白字符串“ ”,不存在的Id

voter值

非空且已报名且未投票的用户

Null,空字符串“”,空白字符串“ ”

业务约束(依赖id有效的前提下)

约束条件

有效等价类

无效等价类

邀请是否存在

FindInvitationById返回非null

findInvitationById返回null

报名集合findEnrollers

返回null且包含voter的集合

返回null,返回空集合,返回的集合不包含voter

是否已投票findVote

返回false(未投票)

返回true(已投票)

(2)识别等价类 (对每个属性划分有效与无效等价类)

1.有效等价类(已报名+未投票)——>抛出InvitationException

编号

条件

有效等价类

期望结果

1

Id合法且邀请存在

Id非空且findInvitationById返回非null Invitation

——

2

voter合法且已报名

Voter非空且在findEnroller返回的集合中

——

3

未投票过

findvote返回false

成功调用persistVote(id,voter)

2.无效等价类

编号

条件

无效等价类

期望结果

1

Id为null

Id=null

抛出InvitationException(400)

2

id为空字符串

Id=“”

抛出InvitationException(400)

3

id为空白字符串

Id=“ ”

抛出InvitationException(400)

4

邀请不存在

FindInvitationById返回null

抛出InvitationException(404)

5

voter为null

Voter=null

抛出InvitationException(400)

6

voter为空字符串

Voter=“”

抛出InvitationException(400)

7

voter为空白字符串

Voter=“ ”

抛出InvitationException(400)

8

报名集合为null

FindEnroller返回null

抛出InvitationException(403)

9

报名集合为空

FindEnrollers返回空集合

抛出InvitationException(403)

10

用户为报名

FindEnrollers不包含voter

抛出InvitationException(403)

11

重复投票

FindVote返回true

抛出InvitationException(403)

(3)边界值确认

在等价类划分的基础上,我们进一步识别出需要重点测试的边界条件。边界值分析的核心思想是:程序最容易在“临界点”出错——比如空值与长度为1之间、null与空集合之间、true与false之间。因此,我们针对每个输入参数和业务依赖,选取其“刚好越过边界”的关键值进行验证。

1.参数id(邀请ID)——字符串类型

对于字符串参数,边界主要体现在“是否为 null”、“是否为空串”、“是否为空白串”以及“最短有效值”这几个临界点。这些值直接对应代码中的if (id == null || id.isBlank())校验逻辑:

编号

边界值

说明

1

Null

下边界:空值

2

“”

空字符串(长度为0)

3

“ ”

长度为1的空白字符串(刚好时空白的最小长度)

4

“a”

长度为1的非空字符串(刚好有效的最小长度)

5

正常长度的合法ID(如“inv1”)

合法值的典型代表

逻辑:null""" "分别代表“空引用”、“空长度”、“纯空白”三种不同的“无效”形态,确保参数校验的全面性;而"a"则验证了“只要不是空白,哪怕只有 1 个字符也能通过”的有效边界。

2.参数voter(投票人)——字符串类型

voter的边界逻辑与id完全一致,同样围绕isBlank()校验展开:

编号

边界值

说明

1

Null

下边界:空值

2

“”

空字符串(长度为0)

3

“ ”

长度为1的空白字符串

4

“u”

长度为1的非空字符串(刚好有效的最小长度)

5

正常长度的合法用户名(如“hangman”)

合法值的典型代表

3.业务约束边界

除了直接的参数校验,vote()方法还依赖于InvitationRepository的三个方法返回结果。这些依赖的返回值存在多种“边缘状态”,需要单独识别:

编号

边界值

说明

1

findInvitationByI2d返回null

邀请恰好不存在(存在性的边界)

2

FindInvitationById返回有效Invitation

邀请刚好存在(存在性的有效侧)

3

FindEnrollers返回null

报名集合为null

4

findEnrollers返回Set.of()(空集合,size=0)

报名集合刚好为空(无人报名的边界)

5

FindEnrollers返回包含voter的集合(size=1)

刚好只有该用户1人报名(包含关系的最小边界)

6

FindEnrollers返回不包含voter的集合

用户刚好不在报名集合中(不包含的边界)

7

findVote返回false(未投票)

投票状态的有效边界(刚好未投)

8

FindVote返回true(已投票)

投票状态的无效边界(刚好已投过)

逻辑:业务约束边界的选取完全映射了代码中的 3 个关键判断点 ——invitation == nullenrollers == null || !enrollers.contains(voter)findVote(...) == true。每个判断点的“真/假”临界值都必须覆盖,才能保证分支覆盖率的完整性。

4.边界值选取总结

输入/条件

下边界(无效侧)

临界值

Id

Null->“”->“ ”

“a”(最有效)

voter

Null->“”->“ ”

“u”(最短有效串)

邀请存在性

Null(不存在)

非null Invitation(存在)

报名集合

Null->空集合(size=0)

Size=1且含voter

Voter是否报名

集合不含voter

集合含voter(size==1)

是否已投票

True(已投票,无效)

False(未投票,有效)

(4)测试代码实现

1.测试环境初始化(@BeforeEach)

我们需要先构建被测服务的框架。因为InvitationService依赖了InvitationRepository,AttachmentRepository,MemInvitationRepository等数据层组件,我们利用Mockito的mock()方法生成代理对象,并通过构造函数注入到Service中。

2.无效等价类

在voteInvalidData()数据源中,定义了11钟无效场景

然后再想怎样才能让这11个用例在不同的异常下触发,运用了switch(description)动态路由

voteInvalidData()中的第一个参数(如"邀请ID为null")不仅仅是一个展示名称。在testVoteInvalid方法中,我们利用switch(description)将其转化为 Mock 行为的动态路由器。

当 JUnit 5 遍历数据源时,每一条Arguments都会触发一次testVoteInvalid的执行。进入方法体后,switch根据当前的description值,精准地为mockRepository配置对应的when...thenReturn行为——例如遇到"重复投票",就让findVote返回true;遇到"findEnrollers返回null",就让findEnrollers返回null

这种设计的最大好处是:将“变化的部分”(不同场景下的 Mock 设置)与“不变的部分”(统一的异常断言)彻底解耦。未来如果产品经理新增一个“活动已结束不能投票”的约束,我们只需在数据源中追加一行Arguments.of("活动已结束", "inv-end", "userX"),并在switch中新增一个case即可——完全不需要修改已有的测试逻辑,符合开闭原则(OCP)

3.有效等价类

在voteValidData()数据源中只列了一条数据,但它验证了完整的 Happy Path,确保业务逻辑顺利走完并调用了最终的保存方法。

核心是验证repository的persistVote被调用了一次,且参数正确

(5)测试运行效果

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

相关文章:

  • 告别标签通信:用Network Configurator搞定欧姆龙PLC与第三方设备的EIP连接
  • 影视摄影行业数据恢复经典案例全解_东方护航数据恢复深圳店
  • 2026年深度测评:10款好用的降AI率网站,部分无限免费降AI!必备收藏
  • 基于HarmonyOS的选择困难抽签助手应用开发实战
  • SSL/TLS客户端证书认证失败排查:从原理到AI智能修复实践
  • 数据结构基础——第三板块:树与二叉树(Trees Binary Trees)
  • 【亲测释放150多G系统盘空间】Win10 / Win11 系统深度清理教程:如果常规清理方式都无效,看这篇就对了
  • 5分钟快速上手Sunshine:打造免费的个人游戏串流服务器终极指南
  • Zabbix多GPU智能监控解决方案:告别手动运维,实现企业级NVIDIA显卡自动化管理
  • 安全组网供应商前五推荐
  • Jetson边缘嵌入式实战课程第七讲:GStreamer到底是什么,它在Jetson上怎么用
  • 基于 Simulink 的基于 GaN 器件的 MHz 级高频 DC-DC 变换器建模与仿真实战教程
  • 5M风力发电机塔架结构设计与有限元分析
  • 明日方舟素材资源库:一站式获取高清游戏美术资源的完整指南
  • 3分钟完成GTNH汉化:让格雷科技新视野彻底变中文
  • IntelliJ IDEA 提交代码时,不想让 IDE 自动分析代码
  • Kotlin--2--list
  • 智能审计系统(Intelligent Audit System)深度解析:构建基于自动化规则与数据风控的企业级合规检测平台
  • 3个核心功能解析:OCAT如何简化OpenCore配置流程
  • State 深度解析:Reducer、Schema 与多状态设计——从零开始学 LangGraph(二)
  • 第七章-动态规划和遗传算法
  • 股票因子组合怎么避免回测过拟合
  • C++课后习题训练记录Day144
  • AI编程效率提升:从代码生成到工作流自动化的实践
  • S15.3行动触发——降低用户决策的最后阻力
  • 普通投资者做策略复盘时应该记录哪些技术字段
  • 如何将VR视频转换为2D格式:VR-Reversal完整指南
  • 4步构建企业级质量保障体系:Vue.Draggable项目集成Git Hooks自动化检查实战指南
  • 基于HarmonyOS 7.0 跨端开发的沙漠探险装备指南页面实战
  • VMware安装Windows 3.1全攻略:解决声卡驱动与兼容性问题