news 2026/11/3 10:59:07

JUnit单元测试实战:佣金计算器边界值分析与27条用例设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JUnit单元测试实战:佣金计算器边界值分析与27条用例设计

简介:这份实验报告文档面向正在学习软件测试与Java单元测试的学生及初学者,围绕Junit的安装流程与实例应用展开,帮助读者理解单元测试在真实项目中的落地方式。文档以经典的「佣兵佣金计算」问题为载体,完整呈现了Salary类的代码实现、边界值测试方法分析、测试用例设计表以及对应的测试结果,读者可据此掌握如何为业务逻辑编写断言、组织测试用例并验证佣金分段计算的正确性。资源包共1个docx文件,约1.09MB,内容紧凑、代码与结果对照清晰,适合作为课堂实验参考或课后复现练习。目前已有4895人学习下载,说明该案例在软件测试入门学习中具有较高的参考价值,能够帮助读者从零搭建Junit测试环境并完成一次完整的单元测试实践。

1. 从一份佣金计算器说起:为什么单元测试不能只测“正常情况”

佣金计算器是软件测试课里出镜率极高的一个案例:枪机 45 美元、枪托 30 美元、枪管 25 美元,月销量有上限,佣金按 1000 美元、1800 美元两档分段累进。代码不长,逻辑也不复杂,但恰恰是这种“看起来没问题”的类,最容易在边界上翻车。我在带课程设计时见过太多同学,写完totalPrice()和commission()跑一遍main方法,看到控制台打印出 1800 和 220,就认为测试做完了。

这份实验报告要解决的就是这个问题:用 JUnit 把佣金计算逻辑拆成可重复执行的断言,用边界值法把 27 条用例组织成一套能自证清白的测试集。它适合正在做软件测试课设、需要交一份完整实验报告的学生,也适合想把单元测试真正落到 Java 项目里的初级开发者。整份方案的核心不是“会写@Test”,而是想清楚哪些输入值得测、断言该断什么、测试代码和被测代码的边界在哪里。

2. 需求与测试对象拆解:佣金规则里的四类输入域

2.1 业务规则先翻译成可判定的约束

动手写测试之前,得先把自然语言描述翻译成机器能判定的条件。原始需求里藏着三层约束,很多测试用例设计得稀里糊涂,就是因为这三层没分开。

第一层是数量合法性:枪机 1 到 70、枪托 1 到 80、枪管 1 到 90,且销售商每月至少要售出一支完整步枪,意味着三个数量都不能为 0。第二层是金额计算:销售额等于三个数量分别乘以单价再求和。第三层是佣金分段:不超过 1000 美元的部分按 10%,1000 到 1800 美元的部分按 15%,超过 1800 美元的部分按 20%。

把这三层写成判定条件,测试对象就清晰了。totalPrice()负责第一层和第二层,输入越界时返回 -1;commission()依赖totalPrice()的结果做第三层计算,销售额非正时同样返回 -1。注意这里有个设计上的耦合:commission()内部读的是成员变量totalPrice,而不是重新计算,所以调用顺序会影响结果。这个细节在后面写测试时会变成一个坑,先记下来。

2.2 被测类的接口盘点

把Salary类的方法列出来,能测的点其实不多,但每个都有讲究。

方法输入输出可测点
构造方法Salary(int,int,int)三个数量对象实例成员变量是否正确赋值
totalPrice()依赖成员变量销售额或 -1正常求和、越界返回 -1
commission()依赖totalPrice成员佣金或 -1三段累进、非正返回 -1
equals(double,double,Object)期望销售额、期望佣金、对象布尔值实际值与期望值是否一致

这张表暴露了一个问题:equals方法的签名很别扭,它把“期望值”和“被比较对象”混在一起,而且内部又调用了totalPrice()和commission(),等于每次断言都会触发一次计算和一次控制台打印。测试代码里如果大量用它,输出会非常吵。我一般会建议学生保留它作为演示,但真正的断言用assertEquals直接比对返回值,这样失败信息更干净。

2.3 边界值分析:为什么是 27 条而不是 5 条

边界值法的思路是把每个输入域的边界、边界附近、中点都取到。三个数量各有上下界,组合起来数量爆炸,所以实际做法是固定两个变量、变动一个变量,再补几组三变量同时取边界的用例。

报告里给出的 27 条用例,本质上是围绕三个关键金额节点展开的:1000 美元边界、1800 美元边界、以及理论最大值 7800 美元。比如用例 9 到 12 专门卡 1000 美元这条线,用例 17 到 20 卡 1800 美元这条线,用例 25 到 27 则验证越界输入。这种组织方式的好处是,每条用例都能回答“我为什么存在”——要么验证某段佣金比例切换,要么验证越界保护。

提示:边界值用例不是越多越好。27 条里有几条其实是等价的,比如用例 1 到 4 都在验证“略大于最小值”的同一段逻辑,答辩时如果被问到“能不能精简”,要能说出哪几条可以合并、合并后覆盖是否仍然完整。

3. 测试环境搭建:IDEA 里把 JUnit 跑起来的三步

3.1 插件与依赖,别在版本上栽跟头

JUnit 在 IDEA 里的搭建,说穿了就三件事:装插件、加依赖、建测试类。但每年都有同学卡在第一步,现象是Ctrl+Shift+T弹不出“创建测试”的选项,或者弹出来了但@Test标红。

插件层面,在 Settings 的 Plugins 里搜 JUnit 并安装,这一步现在新版 IDEA 基本内置了,搜不到也不用慌。真正关键的是依赖:项目如果是普通 Java 项目而不是 Maven 项目,需要手动把 JUnit 的 jar 包加到模块依赖里。报告里写的是“下载对应版本的 JUnit,并在 Project Structure 中添加模块”,这个描述比较粗,实际操作路径是 File → Project Structure → Modules → Dependencies → 加号 → JARs or directories,选中 junit-4.x.jar 和 hamcrest-core-1.x.jar 两个包。少加 hamcrest 那个,assertThat相关的断言会编译不过。

如果是 Maven 项目,事情简单得多,在pom.xml里加一段依赖即可:

<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency>

scope设为test的含义是,这个依赖只在测试编译和测试运行阶段可见,不会打进最终产物。这一点在答辩时经常被问,答“测试依赖不应该污染主代码”就对了。版本号选 4.x 是因为报告里的测试类继承的是junit.framework.TestCase,这是 JUnit 3 的风格,JUnit 4 仍然兼容;如果换成 JUnit 5,TestCase和@Before的包路径都变了,代码得重写。

3.2 测试类的目录约定

普通 Java 项目没有强制的目录结构,但 IDEA 默认会把测试类放在src/test/java下,和src/main/java平行。如果项目是单模块的简单结构,测试类和被测类放同一个包下也能跑,报告里就是package main下同时放了Salary和SalaryTest。这种放法在课程作业里没问题,但要知道它的代价:测试代码和产品代码混在一起,打包时会一起带走。正规项目里,测试类永远在独立的源码根下,包名与被测类一致,这样既能访问包级私有成员,又不会混入产物。

3.3 生成测试类的骨架

Ctrl+Shift+T在打开Salary.java的情况下按下,IDEA 会提示创建测试,勾选要生成的方法,选 JUnit 4,确认目录,就会得到一个带@Before、@After和若干空@Test方法的骨架。骨架里的setUp和tearDown在这个案例里其实是空的,因为Salary没有需要释放的外部资源。但保留它们有好处:以后如果被测类加了数据库连接或文件句柄,初始化逻辑有地方放,不用改测试结构。

4. 测试代码落地:从一条断言到一套可复跑的用例集

4.1 先修掉被测代码里的两个隐患

直接拿原始Salary类写测试,会遇到两个让人难受的地方,我一般会先做最小改动再测。

第一个是totalPrice()和commission()里的System.out.println。测试跑起来控制台刷屏,失败时真正的断言信息被淹没。第二个是commission()依赖totalPrice成员变量,如果测试里只调commission()不调totalPrice(),totalPrice还是默认值 0,佣金直接返回 -1。这不是被测代码的 bug,是调用契约没写清楚,但测试必须遵守这个契约。

处理方式有两种:一是在测试的setUp里先调一次totalPrice()把成员变量算出来;二是干脆在断言里同时验证两个方法。报告里的测试代码用的是后者,通过equals(1800, 220, salary)一次性比对销售额和佣金。这种写法紧凑,但失败时只能知道“不相等”,不知道是销售额错了还是佣金错了。我的习惯是拆成两条断言:

@Test public void testBoundary1800() { // 18 个枪机、18 个枪托、18 个枪管,销售额正好 1800 Salary salary = new Salary(18, 18, 18); // 先触发销售额计算,commission 依赖该成员变量 double total = salary.totalPrice(); double commission = salary.commission(); // 1800 美元时佣金 = 1000*0.1 + 800*0.15 = 220 assertEquals(1800.0, total, 0.001); assertEquals(220.0, commission, 0.001); }

assertEquals的第三个参数是误差容限。金额用double存储,累进计算里有多处乘法,浮点误差几乎必然出现,比如 0.1 加 0.15 这类运算在二进制下不精确。容限设 0.001 意味着差一分钱以内都算通过,这个粒度对美元金额是合理的。如果不设容限,用两参数版本,某条用例可能因为 219.99999999 和 220.0 不相等而“假失败”,这种失败最消耗调试时间。

4.2 用参数化思路组织 27 条用例

27 条用例逐条写@Test方法,代码会膨胀到两三百行,而且改一个期望值要翻半天。JUnit 4 的参数化运行器能把输入和期望值抽成数据表,测试方法只写一遍。改造后的结构大致是这样:

@RunWith(Parameterized.class) public class SalaryParamTest { private int locks, stocks, barrels; private double expectedTotal, expectedCommission; // 每组数据:三个数量 + 期望销售额 + 期望佣金 @Parameterized.Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][]{ {1, 1, 1, 100, 10}, // 最小值 {10, 10, 10, 1000, 100}, // 1000 边界 {18, 18, 18, 1800, 220}, // 1800 边界 {70, 80, 90, 7800, 1420}, // 理论最大值 {71, 80, 90, -1, -1} // 枪机越界 }); } public SalaryParamTest(int l, int s, int b, double et, double ec) { this.locks = l; this.stocks = s; this.barrels = b; this.expectedTotal = et; this.expectedCommission = ec; } @Test public void testCommission() { Salary salary = new Salary(locks, stocks, barrels); assertEquals(expectedTotal, salary.totalPrice(), 0.001); assertEquals(expectedCommission, salary.commission(), 0.001); } }

这段代码的关键在@Parameterized.Parameters标注的静态方法,它返回一个集合,集合里每个Object[]就是一组测试数据,JUnit 会为每组数据新建一次测试类实例并跑一遍testCommission。构造函数负责把数据灌进成员变量,测试方法里只写断言逻辑。27 条用例变成 27 行数据,增删改都在一处,可读性比 27 个方法高一个量级。

注意:参数化测试里不要用@Before去初始化被测对象,因为@Before在每组数据前都会跑,但构造函数的参数注入发生在@Before之前还是之后,不同 JUnit 版本行为有差异。稳妥做法就是在@Test方法里new对象,像上面那样。

4.3 越界用例的断言怎么写才不误导

越界输入时,totalPrice()返回 -1,commission()也返回 -1。但要注意,commission()返回 -1 的原因不是“佣金算出来是负的”,而是“销售额非正,拒绝计算”。测试里如果只断言assertEquals(-1, salary.commission(), 0.001),通过是能通过,但没验证到“越界时totalPrice确实返回了 -1”这个前置条件。

更严谨的写法是先断言totalPrice为 -1,再断言commission为 -1,两条断言都过,才能说明越界保护在两个方法上都生效了。报告里用例 26 和 27 的注释写的是“输入错误”“不满足条件”,对应的就是这类场景。答辩时如果被追问“你怎么证明越界时没有算出错误金额”,答案就是这两条断言的存在。

4.4 测试结果怎么读

跑完测试,IDEA 的 Run 窗口会给出一棵结果树,绿色对勾是通过,红色叉号是失败。失败时点开某条用例,能看到期望值和实际值的对比,比如expected:<220.0> but was:<219.999>,这种就是前面说的浮点误差,加容限即可。如果是expected:<220.0> but was:<-1.0>,说明被测对象根本没算出销售额,回去检查是不是漏调了totalPrice()。

报告里贴了 27 张运行结果截图,实际交付时不需要这么多,挑三张有代表性的就够:一张正常通过、一张边界通过、一张越界通过。截图要能看清测试方法名和断言结果,别只截一个绿条。

5. 避坑与排查:单元测试里最容易翻车的五个点

5.1 现象:测试全绿,但换个顺序跑就红

原因:测试方法之间有隐式依赖。Salary的commission()读成员变量totalPrice,如果某个测试先调了totalPrice()把值算出来,后面的测试不调也能拿到非零值,看起来通过了;但 JUnit 不保证方法执行顺序,单独跑那条测试就失败。

解决:每条测试自己负责把前置状态准备好,不依赖其他测试的副作用。具体到本例,每个@Test里都先new Salary(...),再显式调totalPrice(),再断言commission()。测试之间零共享,顺序就无关紧要了。

5.2 现象:断言用==比较double,偶尔失败

原因:浮点数二进制表示不精确,0.1 + 0.2 != 0.3这个经典问题在佣金累进计算里一定会遇到。

解决:所有金额断言统一用assertEquals(expected, actual, delta)三参数版本,delta取 0.001 或 0.01。不要用assertTrue(a == b),也不要用equals方法内部直接==比较double——报告里Salary.equals就是这么写的,它作为演示可以,作为断言依据不可靠。

5.3 现象:@Test标红,提示 “Cannot resolve symbol”

原因:JUnit 依赖没加对。常见情况是只加了 junit jar,没加 hamcrest;或者 Maven 项目里scope写成了provided,测试阶段能编译但运行时报NoClassDefFoundError。

解决:普通项目检查 Project Structure 的 Dependencies 里两个 jar 都在;Maven 项目检查pom.xml的scope是test,然后执行mvn dependency:tree确认依赖真的被解析进来了。IDEA 右侧 Maven 面板点刷新,别只改文件不刷新。

5.4 现象:测试类继承了TestCase,但@Before不生效

原因:JUnit 3 和 JUnit 4 的注解体系混用。junit.framework.TestCase是 JUnit 3 的基类,它认的是setUp()方法名,不认@Before注解。类继承了TestCase又用 JUnit 4 注解,运行器可能按 JUnit 3 的方式跑,注解被忽略。

解决:二选一。要么完全走 JUnit 4,不继承TestCase,用@Before/@After;要么完全走 JUnit 3,方法名用setUp/tearDown,不加注解。报告里的测试类两者混用,能跑通是因为setUp和tearDown方法名恰好符合 JUnit 3 约定,但这是巧合,不是正确姿势。新写的测试类一律不继承TestCase。

5.5 现象:越界用例期望 -1,实际也是 -1,但测试没覆盖到真正的分支

原因:totalPrice()里越界判断和正常计算写在同一个if-else里,返回 -1 之前还打印了一行“输入有错误”。如果测试只断言返回值,没验证“正常分支没被执行”,那么即使把if条件写反,某些用例也可能碰巧通过。

解决:越界用例除了断言返回 -1,还要断言正常计算的副作用没有发生。本例里副作用是控制台输出,不好断言,那就退一步:确保越界用例的输入在三个维度上分别越界(枪机超、枪托超、枪管超各一条),而不是只测一个维度。三个维度都覆盖,条件写反时至少有一条会红。

6. 验收与答辩:怎么证明这套测试真的有效

验收一套单元测试,不看测试方法有多少个,看三件事:覆盖率、断言质量、可重复性。覆盖率用 IDEA 自带的 Run with Coverage 就能跑,Salary类的行覆盖率达到 100% 是及格线,分支覆盖率要重点看commission()里的三个if分支是否都被走到——1000 以下、1000 到 1800、1800 以上,各至少一条用例。如果分支覆盖率卡在 75% 上不去,多半是漏了某段佣金区间的用例。

断言质量看失败信息。把某条用例的期望值故意改错,跑一遍,看报错信息能不能让人一眼定位问题。好的断言失败信息应该长这样:expected:<220.0> but was:<215.5>,配合方法名testBoundary1800,不用翻代码就知道是 1800 边界那条算错了。如果失败信息是AssertionError加一堆堆栈,说明断言写得太粗。

可重复性最简单:连续跑三遍,结果一致;打乱方法顺序跑,结果一致;换一台机器跑,结果一致。前两条在 IDEA 里点几次就行,第三条在答辩现场如果条件允许,可以当场用命令行mvn test跑一遍,比截图有说服力。

答辩时最常被问的问题是“你的测试用例怎么设计的”。回答框架我一般建议按这个顺序:先说被测方法的输入域有哪几个维度,再说每个维度的边界值怎么取,最后说哪些用例是等价的、为什么保留。27 条用例不用逐条讲,挑 1000 边界、1800 边界、最大值、越界四类各讲一条,每条说清楚“输入是什么、期望是什么、验证了哪段逻辑”。如果老师追问“能不能减少用例”,就指出用例 1 到 4 覆盖的是同一段佣金逻辑,可以合并成一条,但合并后要补一条中点用例保证区间内也正确。

有个细节容易被忽略:报告里的测试代码只写了一条testTotalSalary,实际跑的是equals(1800, 220, salary),输入是 10、20、30,算出来销售额是 1800,佣金 220。这条用例本身没问题,但它同时验证了两个方法,失败时定位困难。答辩时如果被问到“这条测试到底测了什么”,要能拆开说:销售额断言验证了 10×45+20×30+30×25=1800,佣金断言验证了 1000×0.1+800×0.15=220。能拆开讲,说明真懂了。

从那以后我每次带学生做测试课设,都要求他们先把被测类的调用契约写在一张纸上——哪个方法依赖哪个成员变量、调用顺序有没有要求——再动手写用例。契约没理清就写测试,写出来的多半是“能跑但证明不了什么”的摆设。希望这套从需求拆解到验收答辩的流程,能帮你把那份实验报告从“交作业”变成“拿得出手的方案”。

本文还有配套的精品资源,点击获取

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

MBA学术研究必备:9大AI工具提升论文效率

1. 项目概述&#xff1a;MBA学术研究的AI工具革命 作为一名经历过MBA论文煎熬的过来人&#xff0c;我深刻理解查找优质文献时的痛苦。传统学术数据库检索效率低下&#xff0c;文献质量参差不齐&#xff0c;而AI技术正在彻底改变这一现状。本文将分享9个经过实战检验的AI学术平台…

作者头像 李华
网站建设 2026/11/3 10:58:52

SSM+微信小程序宠物寄养系统毕业设计源码(含论文与数据库)

简介&#xff1a;本资源是一份完整的本科毕业设计论文&#xff0c;面向计算机相关专业学生及微信小程序开发初学者&#xff0c;聚焦宠物寄养服务场景的信息化解决方案。论文全文约一万字&#xff0c;系统阐述了基于微信小程序的宠物寄养平台从需求分析、技术选型&#xff08;前…

作者头像 李华
网站建设 2026/11/3 10:58:51

AI 学习第 3 课:手推线性回归与梯度下降,numpy 跑出 loss 曲线

第 2 课我们把数据洗干净、切好了训练集和验证集。这一课在这份数据上做第一件"像机器学习"的事&#xff1a;拟合一条&#xff08;这里是超平面&#xff09;y Xw b。全程不装 PyTorch、不装 sklearn&#xff0c;只用 numpy 把梯度算出来、把 loss 曲线跑出来&#…

作者头像 李华
网站建设 2026/11/3 10:58:44

DeepSeek-R1实战指南:零基础搞定办公、数据与代码三大痛点

简介&#xff1a;本资源是清华大学新闻与传播学院新媒体研究中心元宇宙文化实验室出品的104页深度学习实践指南&#xff0c;面向AI初学者、NLP开发者及大模型应用从业者&#xff0c;系统解决DeepSeek模型认知、场景落地与人机协同能力构建三大核心问题。内容覆盖DeepSeek公司定…

作者头像 李华
网站建设 2026/11/3 10:58:01

SolidWorks二次开发:VBA宏改造与特征装配自动化

简介&#xff1a;这份面向SolidWorks二次开发学习者与工程师的教程文档&#xff0c;围绕API、VBA与宏自动化展开&#xff0c;适合希望简化建模流程、提升设计效率并建立二次开发基础的初、中级读者。压缩包内共1个doc文件&#xff0c;大小约276KB&#xff0c;以系列教程形式串联…

作者头像 李华
网站建设 2026/11/3 10:56:01

神经网络画图PPT模版:从图形语义到结构复用的设计指南

简介&#xff1a;这份神经网络画图PPT模版面向机器学习学习者、论文撰写者与高校教师&#xff0c;用于在学术报告、课堂教学和论文配图中直观呈现神经网络结构与工作原理&#xff0c;解决手绘示意图耗时、结构不统一的问题。压缩包内共1个pptx文件&#xff0c;约28.87MB&#x…

作者头像 李华