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()负责第一层和第二层输入越界时返回 -1commission()依赖totalPrice()的结果做第三层计算销售额非正时同样返回 -1。注意这里有个设计上的耦合commission()内部读的是成员变量totalPrice而不是重新计算所以调用顺序会影响结果。这个细节在后面写测试时会变成一个坑先记下来。2.2 被测类的接口盘点把Salary类的方法列出来能测的点其实不多但每个都有讲究。方法输入输出可测点构造方法Salary(int,int,int)三个数量对象实例成员变量是否正确赋值totalPrice()依赖成员变量销售额或 -1正常求和、越界返回 -1commission()依赖totalPrice成员佣金或 -1三段累进、非正返回 -1equals(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 里的搭建说穿了就三件事装插件、加依赖、建测试类。但每年都有同学卡在第一步现象是CtrlShiftT弹不出“创建测试”的选项或者弹出来了但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 groupIdjunit/groupId artifactIdjunit/artifactId version4.13.2/version scopetest/scope /dependencyscope设为test的含义是这个依赖只在测试编译和测试运行阶段可见不会打进最终产物。这一点在答辩时经常被问答“测试依赖不应该污染主代码”就对了。版本号选 4.x 是因为报告里的测试类继承的是junit.framework.TestCase这是 JUnit 3 的风格JUnit 4 仍然兼容如果换成 JUnit 5TestCase和Before的包路径都变了代码得重写。3.2 测试类的目录约定普通 Java 项目没有强制的目录结构但 IDEA 默认会把测试类放在src/test/java下和src/main/java平行。如果项目是单模块的简单结构测试类和被测类放同一个包下也能跑报告里就是package main下同时放了Salary和SalaryTest。这种放法在课程作业里没问题但要知道它的代价测试代码和产品代码混在一起打包时会一起带走。正规项目里测试类永远在独立的源码根下包名与被测类一致这样既能访问包级私有成员又不会混入产物。3.3 生成测试类的骨架CtrlShiftT在打开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 CollectionObject[] 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()返回 -1commission()也返回 -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×4520×3030×251800佣金断言验证了 1000×0.1800×0.15220。能拆开讲说明真懂了。从那以后我每次带学生做测试课设都要求他们先把被测类的调用契约写在一张纸上——哪个方法依赖哪个成员变量、调用顺序有没有要求——再动手写用例。契约没理清就写测试写出来的多半是“能跑但证明不了什么”的摆设。希望这套从需求拆解到验收答辩的流程能帮你把那份实验报告从“交作业”变成“拿得出手的方案”。本文还有配套的精品资源点击获取