简介:这份实验报告文档面向软件测试与Java单元测试初学者,围绕JUnit的安装配置和实例应用展开,帮助读者理解单元测试从环境搭建、测试类编写到结果验证的完整流程。压缩包内共1个docx文件,大小约1.09MB,内容以实验报告形式组织,便于直接查阅和整理。文档选用佣金计算案例,给出Salary.java的枪机、枪托、枪管销售金额与分段佣金计算代码,并结合边界值方法列出测试用例表与运行结果,可用来观察正常输入、边界输入及异常提示的处理方式。已有4894人学习下载,适合课程实验、课后练习或自学JUnit时对照参考,重点掌握测试用例设计、断言校验和问题定位思路。
1. 一份 .docx 实验报告:绿条跑出来了,断言就一定站得住吗
绿条亮起来的那一刻,多数人以为 JUnit 单元测试已经做完了。真正的问题在后面:这条绿到底证明了什么。手头这份实验报告值得拆,原因不在代码多漂亮,而在它把「被测类 → 业务规则 → 边界值用例表 → 测试代码 → 运行结果 → 反思」六段完整串成了一条链,26 条输入组合的期望值全部写死在表格里,任何一个数字都能手算复核。它适合两类人:要交软件测试课设、被要求「必须用 JUnit」的学生,以及接手一个只有main方法、想补第一层测试网的老项目维护者。下文按这份报告的原始顺序往下推,先讲清佣金模型的输入域,再落到环境搭建、用例收敛和踩过的坑。
2. 把佣金问题拆成可测单元:45/30/25 单价与 1000/1800 两条累进线
这份报告的被测对象是一个销售佣金计算器:某经销商销售三类部件,部件 A 单价 45 美元,部件 B 单价 30 美元,部件 C 单价 25 美元;每月每个部件有销售限额,A 最多 70 件、B 最多 80 件、C 最多 90 件;并且每月至少要售出一套完整组合,也就是三类部件各至少 1 件。月末汇总三类部件的数量,算出总销售额,再按累进费率算佣金。这个模型参数少、规则硬、期望值可手算,是练手 JUnit 单元测试的理想样本——它的价值不在于业务本身,而在于它天然包含「有效等价类」「无效等价类」「累进分段」三类典型结构。
2.1 业务规则先落成公式:100 美元和 220 美元两个台阶
报告里给出的佣金规则是三段累进,不是一刀切。销售额不超过 1000 美元的部分按 10% 计,1000 到 1800 美元之间的部分按 15% 计,超过 1800 美元的部分按 20% 计。注意「累进」这两个字:第二段只对超出 1000 的那部分加价,第一段那 1000 美元的 10% 不会因为跨档而重新计算。
把它们写成式子,被测逻辑就清楚了:
S = 45 * A + 30 * B + 25 * C # 总销售额,A/B/C 为三类部件数量 S <= 1000 : commission = S * 0.10 1000 < S <= 1800 : commission = 1000 * 0.10 + (S - 1000) * 0.15 S > 1800 : commission = 1000 * 0.10 + 800 * 0.15 + (S - 1800) * 0.20两处台阶值要背下来:S = 1000 时佣金恰好 100 美元,S = 1800 时佣金恰好 220 美元。这两个数字是所有边界用例的锚点,写测试时以它们为基准往上加减一个「最小可构造步长」,比凭感觉凑数字靠谱得多。
再看全区间:输入域下限是 A=B=C=1,此时 S = 45 + 30 + 25 = 100 美元,佣金 10 美元;上限是 A=70、B=80、C=90,此时 S = 3150 + 2400 + 2250 = 7800 美元,佣金 = 220 + (7800 - 1800) × 0.2 = 1420 美元。这一头一尾正好对应报告用例表里的第 1 行和第 26 行,说明那张表不是随手编的,而是先算了极值再往里插点。
| 区间 | 销售额范围 | 该段费率 | 累计佣金上界 |
|---|---|---|---|
| 第一档 | 0 < S ≤ 1000 | 10% | 100 |
| 第二档 | 1000 < S ≤ 1800 | 15% | 220 |
| 第三档 | S > 1800 | 20% | 220 + (S − 1800) × 0.2 |
这张表在写断言时直接当期望值来源用。比如测 1800 这个点,期望佣金写 220,不要写 220.0 之外的任何近似值;测 1825 这个点,期望写 225,因为 25 × 0.2 = 5。
2.2 输入域的四个约束:三组限额加一条「至少一套」
很多人写这类测试只盯着金额分段,忘了数量本身的约束,结果越界用例一条都没有。原始约束有四个:
- 1 ≤ A ≤ 70,A 为整数;
- 1 ≤ B ≤ 80,B 为整数;
- 1 ≤ C ≤ 90,C 为整数;
- 每月至少售出一套完整组合,即三类各至少 1 件。
前三条是区间约束,第四条其实是前三条下限的合并,但由于业务上强调「至少一套完整」,在用例设计时它值得单列一条注释,因为它解释了下限为什么是 1 而不是 0。这一点直接影响无效等价类的划分:A=0、B=80、C=90 属于「数量越界」,而 A=1、B=1、C=1 属于「刚好满足完整性」的有效最小值,两者的期望结果天差地别,一个是错误提示,一个是佣金 10 美元。
原始代码用一行联合条件把四个约束全塞进去了:
if (locks >= 1 && locks <= 70 && stocks >= 1 && stocks <= 80 && barrels >= 1 && barrels <= 90) { totalPrice = locks * lockPrice + stocks * stockPrice + barrels * barrelPrice; return totalPrice; } else { System.out.println("输入有错误!!!"); return -1; }这行if的短路求值顺序是 A 的上限、B 的上限、C 的上限依次判断,写测试时如果只改一个变量,短路会让后面的条件根本没被执行到。换句话说,分支覆盖看起来满了,条件覆盖其实缺了一大块。想把这块补上,就得设计「只越 A 的界」「只越 B 的界」「只越 C 的界」三条独立用例,而不是一条 A=70、B=81、C=90 全越的组合。报告用例表的第 26、27 行分别给了70 81 90和0 80 90,前者只越 B 的界,后者只越 A 的下界,思路是对的,但还差一条只越 C 的界。
2.3 顺序耦合:单独调 commission() 为什么会返回 −1
这是原始类里最隐蔽的一处设计问题,也是初学者测试跑红时最容易被误导的地方。看commission()的开头:
public double commission() { if (totalPrice > 0) { // ... 按 totalPrice 分段计算 return commission; } else { System.out.println("输入有错误!!!"); return -1; } }它读的是成员变量totalPrice,而totalPrice只在totalPrice()方法被调用时才被赋值。构造完对象直接调commission(),totalPrice还是默认值 0.0,条件不成立,方法返回 −1 并打印「输入有错误」。新手看到这个结果,第一反应往往是「构造器写错了」,然后在构造器里翻半天——其实构造器没问题,问题在于两个方法之间存在隐式调用顺序。
报告里的测试代码之所以能过,某种程度上是巧合:
Assert.assertTrue(salary.equals(1800, 220, salary));而equals内部是salary.totalPrice() == etotalPrice && salary.commission() == ecommission,&&从左往右求值,先调totalPrice()把字段填上,再调commission()就拿到了正确的值。换个写法,比如先断言佣金再断言总额,整条测试立刻翻车。
提示:这类隐式状态依赖是单元测试里最常见的「翻车点」之一。被测方法之间有顺序要求,测试就必须显式体现这个顺序,或者在重构时把它消掉。
稳妥的做法有两个方向。方向一是让测试显式遵守顺序,assert总价在前、佣金在后,并在测试方法上写清注释;方向二是把计算逻辑改成纯函数,入参即全部依赖:
public class Salary { private static final int PRICE_A = 45; private static final int PRICE_B = 30; private static final int PRICE_C = 25; private final int a, b, c; public Salary(int a, int b, int c) { this.a = a; this.b = b; this.c = c; } /** 校验输入域,越界直接抛异常,而不是打印后返回 -1 */ public void validate() { if (a < 1 || a > 70) throw new IllegalArgumentException("部件A数量越界: " + a); if (b < 1 || b > 80) throw new IllegalArgumentException("部件B数量越界: " + b); if (c < 1 || c > 90) throw new IllegalArgumentException("部件C数量越界: " + c); } /** 纯函数:不依赖任何外部状态,随时可调 */ public double totalPrice() { validate(); return a * PRICE_A + b * PRICE_B + c * PRICE_C; } /** 纯函数:入参是销售额,不再读成员字段 */ public double commission(double total) { if (total <= 1000) return total * 0.10; if (total <= 1800) return 100.0 + (total - 1000) * 0.15; return 220.0 + (total - 1800) * 0.20; } }改造点有三个:常量用private static final固定,避免测试期间被意外改写;validate()单拎出来,越界用异常表达,测试可以用assertThrows精确断言;commission(double)改成接收参数,调用它不再需要先调totalPrice(),顺序耦合消失。这三处改完,测试代码的写法立刻从「小心翼翼排顺序」变成「闭着眼睛随便调」。
3. 在 IDEA 里把 JUnit 环境搭起来:插件、依赖、测试源根三件事的先后顺序
报告里给的搭建步骤是「装插件 → 下载 JUnit 加模块 → 新建测试项目 → Ctrl+Shift+T」,方向没错,但只说了一半。实际卡住新手的地方几乎都不在插件上,而在依赖坐标和目录标记上。我按真正会出问题的顺序重排一遍。
3.1 插件只是 IDE 侧的壳,编译期靠的是依赖
IDEA 的 JUnit 插件提供的是「生成测试类」「右键运行」这些 IDE 能力,它不含junit.jar。代码里写import org.junit.Test;能通过语法高亮,但一编译就报程序包org.junit不存在,原因就是编译期 classpath 上没有这个包。
路径一:Maven/Gradle 工程(推荐)。在pom.xml里加依赖,作用域必须是test:
<dependencies> <!-- JUnit 4 写法 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <!-- 或者 JUnit 5 写法,二选一,不要混着引 --> <!-- <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.2</version> <scope>test</scope> </dependency> --> </dependencies><scope>test</scope>的含义是:这个依赖只在编译和运行测试代码时可见,打包进生产产物时会被排除。把它写成compile不算错,但会把测试框架带进发布包,属于典型的规范问题。
路径二:非 Maven 的普通 Java 工程。这时候要手动把 jar 加到模块依赖里。报告里「下载对应版本的 Junit 并在 Project Structure 中添加模块 Junit」说的就是这一步。操作次序是File → Project Structure → Modules → Dependencies → + → JARs or directories,选中下载好的junit-4.13.2.jar和hamcrest-core-1.3.jar(JUnit 4 的断言依赖 Hamcrest,漏了它会在运行期报NoClassDefFoundError: org/hamcrest/SelfDescribing),作用域同样选Test。
提示:JUnit 4 需要 hamcrest-core,JUnit 5 不需要。这两个 jar 一起下载、一起加,比事后看着
NoClassDefFoundError猜要快得多。
版本选择上给一条硬规矩:项目里 JDK 8 就配 JUnit 4.13.2,JDK 11 及以上可以上 JUnit 5。不要在同一模块里既引 JUnit 4 又引 JUnit 5,除非你明确知道 Vintage 引擎在做什么,否则@Test注解读哪个包会变成一个玄学问题。
3.2 测试源根与包名:Ctrl+Shift+T 找不到类的真实原因
Ctrl+Shift+T(Windows/Linux)或Cmd+Shift+T(macOS)是 IDEA 的「创建测试」快捷键,光标停在被测类名上按下即可。它失效通常只有两个原因。
第一个原因是目录没被标记为测试源根。Maven 约定测试代码放在src/test/java,IDEA 需要在Project Structure → Modules → Sources里把该目录标记为Test Sources(目录图标变绿)。没标记的目录,IDEA 视为普通目录,生成的测试类不会进入测试 classpath,右键跑的时候会提示No tests found。
第二个原因是包名对不上。IDEA 生成测试类时会默认带上与被测类相同的包声明。如果被测类在package main;下(报告里就是这样),测试类也必须写package main;,并且物理路径要与包名一致。手工把测试类挪到别的包,@Test方法依然能编译,但反射查找时可能找不到,表现同样是No tests found。
目录结构长这样:
demo-project/ ├── pom.xml └── src/ ├── main/ │ └── java/ │ └── main/ │ └── Salary.java └── test/ └── java/ └── main/ └── SalaryTest.java包名main是个历史包袱,报告里沿用了,实际项目里建议改成com.example.salary这类有域名的包名,避免和其他模块冲突。
3.3 一份能独立跑起来的最小测试骨架
报告里的测试类同时继承了junit.framework.TestCase(JUnit 3 的写法)又用了@Before、@Test(JUnit 4 的写法),这是典型的混搭。它能跑,是因为 JUnit 4 检测到TestCase子类时会退回JUnit38ClassRunner,按方法名前缀test去找用例,@Test注解形同虚设,@Before也不会执行。
规范写法二选一。这里给 JUnit 4 的完整骨架,能直接在 IDEA 里跑:
package main; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; public class SalaryTest { private Salary salary; @Before // 每个 @Test 执行前都会跑一次 public void setUp() { salary = new Salary(10, 20, 30); // A*45+B*30+C*25 = 1800 } @After // 每个 @Test 执行后都会跑一次,用于释放资源 public void tearDown() { salary = null; } @Test public void testTotalPriceAtBoundary() { assertEquals(1800.0, salary.totalPrice(), 1e-9); } @Test public void testCommissionAtBoundary() { assertEquals(220.0, salary.commission(salary.totalPrice()), 1e-9); } @Test public void testOutOfRangeShouldThrow() { Salary bad = new Salary(70, 81, 90); assertThrows(IllegalArgumentException.class, bad::validate); } }三点说明。@Before里初始化被测对象,是为了让每个测试方法拿到干净实例,避免用例之间互相污染;@After里置空是习惯动作,对简单对象意义不大,但涉及文件流、数据库连接时必须写。assertEquals的三个参数依次是期望值、实际值、容差,第三个数不能省——浮点比较省掉容差是另一类经典翻车,第 4 章展开。
4. 边界值用例落到断言上:26 条输入组合怎么变成一张参数化表
报告最值钱的部分是那张 26 行的用例表,每一行都有 ID、三类部件数量、期望销售额、期望佣金和注释。问题在于它的测试代码只写了locks=10, stocks=20, barrels=30一条,表格和代码完全脱节。这一章讲怎么把表原样搬进代码。
4.1 边界点的可构造性:为什么 1025 比 1001 更值得测
纯数学上的边界值测试会说「1000 的两侧取 999 和 1001」。这个模型里行不通。因为三类部件的单价是 45、30、25,最大公约数是 5,任何组合的销售额必然是 5 的整数倍,999、1001、1801 这些数字根本构造不出来。硬要写进期望值,只会得到一条永远失败的用例。
正确做法是先算出「可达金额的最小步长」,再在边界上取最近的可达点。1000 的上一档是 1005,用1×45 + 2×30 + 8×25 = 45 + 60 + 200 = 305不行,得凑。报告里给的是 1025(10,10,11:450 + 300 + 275 = 1025)和 975(10,10,9:450 + 300 + 225 = 975),这两个点距 1000 分别是 +25 和 −25,是当时能凑出的最近距离。1800 那条线同理,用了 1775(18,17,18)和 1825(18,18,19)。
我把这张表里最有代表性的几行挑出来核对一遍,方便抄的时候对答案:
| 部件 A | 部件 B | 部件 C | 销售额 | 佣金 | 用例定位 |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 100 | 10 | 有效最小值 |
| 5 | 5 | 5 | 500 | 50 | 第一档中点 |
| 10 | 10 | 10 | 1000 | 100 | 第一档上边界 |
| 10 | 10 | 11 | 1025 | 103.75 | 刚越过第一档边界 |
| 14 | 14 | 14 | 1400 | 160 | 第二档中点 |
| 18 | 18 | 18 | 1800 | 220 | 第二档上边界 |
| 18 | 18 | 19 | 1825 | 225 | 刚越过第二档边界 |
| 48 | 48 | 48 | 4800 | 820 | 第三档中点 |
| 70 | 80 | 90 | 7800 | 1420 | 有效最大值 |
| 70 | 81 | 90 | 越界 | 错误 | 仅越 B 的上限 |
| 0 | 80 | 90 | 越界 | 错误 | 仅越 A 的下限 |
按「每个边界取上、中、下三点,每档补一个中点,再给三条单变量越界」的规则,这张表从 26 行压到 11 行左右,边界覆盖率一点没掉,反而因为是按规则生成,未来参数一改还能自动扩展。
4.2 用参数化测试把 26 行表格搬进代码
JUnit 5 的@ParameterizedTest配@CsvSource是最省事的方案,一行 CSV 就是一条用例:
package main; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class SalaryParameterizedTest { /** 有效用例:四个入参依次是 A/B/C/期望佣金,销售额由代码内部算,不写死 */ @ParameterizedTest(name = "[{index}] A={0} B={1} C={2} 期望佣金={3}") @CsvSource({ " 1, 1, 1, 10.00", // 有效最小值 " 5, 5, 5, 50.00", // 第一档中点 "10, 10, 10, 100.00", // 第一档上边界 "10, 10, 11, 103.75", // 刚越过第一档 "14, 14, 14, 160.00", // 第二档中点 "18, 18, 18, 220.00", // 第二档上边界 "18, 18, 19, 225.00", // 刚越过第二档 "48, 48, 48, 820.00", // 第三档中点 "70, 80, 90, 1420.00" // 有效最大值 }) void testCommission(int a, int b, int c, double expected) { Salary s = new Salary(a, b, c); double total = s.totalPrice(); assertEquals(expected, s.commission(total), 1e-6); // 容差见 4.3 } /** 无效用例:单变量越界,应当抛异常 */ @ParameterizedTest(name = "[{index}] A={0} B={1} C={2} 应越界") @CsvSource({ "70, 81, 90", // 仅 B 越上限 " 0, 80, 90", // 仅 A 越下限 "70, 80, 91" // 仅 C 越上限 }) void testOutOfRange(int a, int b, int c) { Salary s = new Salary(a, b, c); assertThrows(IllegalArgumentException.class, s::validate); } }参数说明几处。name = "[{index}] ..."让运行面板里直接显示这一行的入参,失败时不用数第几行。@CsvSource默认会去掉每个值前后的空白,所以" 1, 1, 1, 10.00"里的空格不用管。有效用例里期望佣金写死、销售额不写死,是为了让断言只盯佣金——销售额算错会连带佣金错,报错信息里能一眼看出是哪个环节出的问题,如果两个都写死,失败时就得多算一遍。
越界用例单独一个方法,原因是失败语义不同:有效用例失败说明计算逻辑错,无效用例失败说明校验逻辑漏了。混在一个方法里,跑红之后第一眼判断不出问题在哪。
4.3 浮点断言与容差:103.75 这类金额怎么写才不飘
这是最容易让人怀疑人生的地方。(1025 - 1000) * 0.15在 double 下不是 3.75,而是 3.7499999999999996,加上 100 之后是103.74999999999999。如果断言写成:
assertEquals(103.75, s.commission(total)); // 编译不过(JUnit 5 无此重载),JUnit 4 下则直接失败在 JUnit 4 里assertEquals(double, double)已被标记为废弃,实际走的是精确比较,这条用例必红。这不是代码写错了,是二进制浮点表示 0.15 的固有误差。
三种处理方式,按场景选:
- 加容差。
assertEquals(103.75, actual, 1e-6),第六位小数以内都算对。佣金金额业务上精确到分,1e-6 的容差绰绰有余,0.000001美元不会有人来找你。 - 转整数分。把佣金乘 100 之后
Math.round成long,断言整数相等。适合对精度有合规要求的场景。 - 用
BigDecimal。被测类内部金额直接用BigDecimal存,费率用字符串构造new BigDecimal("0.15"),全程不经过 double。改造量最大,但能根治。
我一般选第一种,理由很简单:被测类本身返回double,测试端用BigDecimal只会在被测类边界上引入一次转换误差,掩盖不了源头问题。要根治得连被测类一起改,那是重构任务,不属于补测试的范围。
5. 避坑与排查:五个让测试「假绿」的细节
绿条不等于有效测试。下面五条都是从这类实验报告里反复见到的真实问题,按「现象 → 原因 → 解决」写,对号入座即可。
5.1 断言与用例层面的三个坑
坑一:用例表 26 行,测试代码只有 1 条。
现象是覆盖率报告里commission()的第二、第三档分支全红,但整个测试类跑起来是绿的。原因是设计和实现脱节:用例在 Word 里设计,代码里只挑了最好算的那一条写。解决方式就是 4.2 的参数化,把表格原样搬进@CsvSource,一条方法吃一整档,改一个数字就多一条用例。
坑二:期望值和被测值挤在一个布尔里。
现象是失败时只报一行AssertionError,看不出是总价错还是佣金错。原始的equals(double etotalPrice, double ecommission, Object object)就是这种写法,两次比较用&&串起来,短路之后后半段根本没执行。原因在于把「两个独立断言」合并成了「一个复合布尔」,丢掉了断言的定位能力。解决是拆开:assertEquals总价一条、佣金一条,两条各自报错,一眼定位。顺带一提,方法名用equals且带三个参数,这是重载而非重写Object.equals,容易让后来的维护者误以为是标准相等判断,改个名能省很多解释成本。
坑三:错误路径只返回 −1,测试只能断言「等于 −1」。
现象是越界用例通过了,但业务上「A 越界」和「B 越界」返回的都是 −1,测试区分不出来,将来有人把 A 的上限从 70 改成 60,这条用例照样绿。原因是方法用「打印 + 返回特殊值」表达错误,而特殊值和正常值共享同一个返回类型,语义被抹平了。解决是改成抛异常,不同变量抛不同消息的IllegalArgumentException,测试端用assertThrows加消息匹配,越界原因一变就能立刻发现。
5.2 环境与执行层面的两个坑
坑四:继承TestCase又写@Before,初始化方法从不执行。
现象是断点打在setUp()里从来进不去,测试方法里的成员变量是 null,触发NullPointerException。原因是 JUnit 4 的运行器检测到类继承自junit.framework.TestCase,会走 JUnit 3 兼容路径JUnit38ClassRunner,只认test前缀的方法名和setUp()/tearDown()这两个固定名字,@Before、@After、@Test全部被忽略。解决是二选一:要么彻底用 JUnit 4,去掉extends TestCase,方法上全用注解;要么彻底用 JUnit 3,把所有方法名前缀改成test,删掉注解。混着写是最坏的选择,因为它能跑,只是行为不是你预期的那个。
坑五:NoClassDefFoundError: org/junit/Test或者No tests found。
前者是依赖没进 classpath,后者是目录没标记。分开说。NoClassDefFoundError出现时先看pom.xml里<scope>是不是写成了provided——provided在测试阶段理论可见,但如果父 POM 里有dependencyManagement覆盖了版本,实际解析出来的可能是空。用mvn dependency:tree -Dscope=test打出来看一眼最直接。No tests found出现时,先确认src/test/java目录在Project Structure里是绿色的 Test Sources,再看包名和物理路径是否一致,最后确认测试类名不是Test开头或结尾之外的怪名字——IDEA 的默认扫描规则是*Test、Test*、*Tests、*TestCase这几种,叫SalaryCheck就跑不起来。
提示:JUnit 4 与 JUnit 5 的断言类不同包,
org.junit.Assert和org.junit.jupiter.api.Assertions混用会编译失败。迁移时整个测试类一起换,别一个文件里两种 import 并存。
6. 把 26 条用例收敛到 9 条:等价类合并与分支覆盖的验证方法
写完参数化表之后,下一步不是继续加用例,而是砍。报告的结尾自己也反思了这点:「应尽量减少测试用例数目,尽可能找到一个和很多测试用例等价的一个用例」。这句话是对的,但没说怎么砍。我的做法是三步。
第一步,按档位分组。第一档 100 到 1000 之间取「最小值、中点、上边界」三个点,第二档 1000 到 1800 同样三个点,第三档 1800 到 7800 取「下边界刚过、中点、最大值」三个点。九条覆盖三档的全部等价类。
第二步,把「刚过边界」的点做成参数化里的偏移量,而不是写死金额。因为单价 45/30/25 的最大公约数是 5,可达金额只能落在 5 的倍数上,边界偏移只能取 5 的整数倍,这一点写成注释比写成代码更省事。
第三步,跑一次带分支覆盖的测试。IDEA 里Run 'SalaryTest' with Coverage,或者 Maven 里配 JaCoCo 出报告,看commission()的三个分支和totalPrice()的两个分支是否都是绿色。收敛后的九条用例应该能把分支覆盖率顶到 100%;如果某一个分支红了,说明该档位缺一个代表点,补回去而不是继续加随机用例。
| 收敛前 | 收敛后 | 覆盖率变化 |
|---|---|---|
| 26 条有效用例 | 9 条 | 分支覆盖 100%,不变 |
| 2 条越界用例 | 3 条 | 条件覆盖从 2/3 提升到 3/3 |
| 1 个测试方法 | 2 个参数化方法 | 失败定位从「整类红」变成「按行报」 |
最后一步验证别省:把commission里的0.15临时改成0.16,重新跑。九条用例里应该有第二档和第三档的用例变红,第一档保持绿。如果全绿,说明期望值是照着实现抄的,不是照着业务规则独立算的——这种测试最危险,它只能防止代码被改坏,不能防止代码一开始就写错。
从那以后我每次把实验报告里的用例表往测试类里搬之前,都先用一段小脚本把可达金额枚举一遍,确认边界点真的构造得出来,再动手写@CsvSource:
# 枚举所有合法组合的销售额,确认边界附近到底有哪些可达值 reachable = sorted({45 * a + 30 * b + 25 * c for a in range(1, 71) for b in range(1, 81) for c in range(1, 91)}) print(reachable[0], reachable[-1]) # 100 7800 print([x for x in reachable if 990 <= x <= 1010]) # 1000 附近的可达点这套动作做过两三次之后就成了肌肉记忆,写测试时手比脑子快。希望帮到你。
本文还有配套的精品资源,点击获取