简介:本资源是一份面向软件工程专业本科生及Java初学者的软件测试实践教学材料,聚焦基本路径测试法原理与JUnit单元测试工具在Eclipse环境下的实操应用。通过自动售货机程序这一典型案例,系统讲解控制流程图绘制、基本路径识别、测试用例设计(含12组输入-预期输出对照表)、JUnit 4.10集成配置(含Build Path操作路径)及缺陷定位修复全过程。资源为1个378KB的Word文档(.doc),完整包含实验目的、环境要求、详细步骤、程序流程图、12组测试用例执行记录、错误截图(图二)、修复后通过截图(图三)、修改前后源码对比及教师评语栏,结构规范、过程可复现。目前已有281人学习下载,适合课堂实验跟进、课程设计参考或自学测试实践,能直接用于搭建可运行的JUnit测试项目并掌握白盒测试核心方法。
1. 基本路径测试不是“走完所有if”,而是用图论锁定最小独立路径集
很多刚接触白盒测试的人会误以为“基本路径测试”就是把每个 if-else 分支都跑一遍——结果写了一堆重复覆盖的用例,却漏掉真正关键的逻辑交汇点。实际上,它本质是控制流图(CFG)上的图论问题:对一个有向图,计算其圈复杂度(Cyclomatic Complexity),再导出一组线性无关的、能覆盖全部可执行路径的最小路径集合。自动售货机这个案例里,operation()方法包含嵌套 if、并列条件判断和多层 return,静态分析得出圈复杂度为 8,意味着至少需要 8 条独立路径才能保证每条逻辑边都被触发一次。但实验报告中只列了 12 个测试用例,其中多个用例实际覆盖的是同一条路径(比如 testOperation1 和 testOperation3 都落在money.equalsIgnoreCase("5C") && type.equals(typeOfGoods[0/1]) && count > 0这一主干路径上),而真正被遗漏的是money.equalsIgnoreCase("1D")分支下countOfFiveCents == 0时的异常跳转路径——这正是图二中报错“Change Shortage”却未被早期用例捕获的根本原因。本篇不讲抽象定义,直接拆解这个 Java 实现:从控制流图绘制、圈复杂度手算、JUnit 测试用例与路径映射关系,到 Eclipse 中真实调试验证,全程可复现。适合正在写课程实验、准备软件测试面试、或接手遗留 Java 系统做回归测试的开发者。
2. 控制流图建模与圈复杂度计算:从源码到路径编号的硬核推演
2.1 手绘控制流图(CFG)的关键节点识别
控制流图不是流程图,它只关注程序执行的逻辑跳转点,忽略数据流和界面交互。以SaleMachine.operation(String type, String money)方法为核心,我们逐行提取决策节点(Decision Point)和终止节点(Exit Point):
- 入口节点:方法开始处(line 36)
- 决策节点1:
money.equalsIgnoreCase("5C")(line 37)→ 两个出口:true / false - 决策节点2(5C分支内):
type.equals(typeOfGoods[0])(line 39)→ true / false - 决策节点3(5C+Beer分支内):
countOfBeer>0(line 41)→ true / false - 决策节点4(5C+OrangeJuice分支内):
countOfOrangeJuice > 0(line 51)→ true / false - 决策节点5(1D分支内):
countOfFiveCents > 0(line 63)→ true / false - 决策节点6(1D+有零钱分支内):
type.equals(typeOfGoods[0])&&countOfBeer>0(line 65)→ true / false - 决策节点7(1D+有零钱+非Beer分支内):
type.equals(typeOfGoods[1])&&countOfOrangeJuice>0(line 71)→ true / false - 终止节点:共 7 个 return 语句(line 45, 49, 55, 59, 75, 83, 91)
提示:Eclipse 自带的PMD 插件或Metrics Plugin可自动生成 CFG 图像,但必须手动校验——因为自动工具常将字符串比较
equalsIgnoreCase误判为非决策点,而此处它正是路径分叉的核心。
2.2 圈复杂度 V(G) 的三种等价计算法
圈复杂度公式为V(G) = E - N + 2P(E=边数,N=节点数,P=连通分量数),但在实际编码中,我们用更可靠的两种方式交叉验证:
方法一:判定节点计数法(最常用)
V(G) = 判定节点数 + 1
本方法中判定节点共 7 个(见 2.1 节),故V(G) = 7 + 1 = 8
方法二:区域计数法(画图验证)
在控制流图中,独立闭合区域(即“环”)数量即为圈复杂度。从 CFG 可数出:
5C → Beer → count>0形成 1 个区域5C → Beer → count<=0形成 1 个区域5C → OrangeJuice → count>0形成 1 个区域5C → OrangeJuice → count<=0形成 1 个区域1D → count>0 → Beer → count>0形成 1 个区域1D → count>0 → Beer → count<=0形成 1 个区域1D → count>0 → OrangeJuice → count>0形成 1 个区域1D → count>0 → OrangeJuice → count<=0形成 1 个区域1D → count<=0形成 1 个区域(注意:此区域独立于上述 8 个)
→ 共 9 个区域?矛盾!
修正:1D → count<=0是单边路径,不构成闭合区域;而1D → count>0 → Type Error是另一条终止路径,需合并到已有区域。最终确认V(G) = 8,与判定节点法一致。
方法三:代码行级验证(防漏判)
使用 Eclipse 的Coverage Tool(需安装 EclEmma)运行全部 JUnit 用例,查看operation()方法的分支覆盖率(Branch Coverage)。若显示为 100%,则说明 8 条路径已全被覆盖;若低于 100%,则缺失路径对应未被执行的if/else组合。
2.3 从圈复杂度导出 8 条基本路径及对应测试输入
路径编号按深度优先顺序生成,每条路径必须包含唯一新增的边(即该路径使总边覆盖数增加):
| 路径编号 | 覆盖的判定序列(T=true, F=false) | 输入 (type, money) | 关键状态约束 | 对应 JUnit 用例 |
|---|---|---|---|---|
| P1 | 5C=T, Beer=T, count>0=T | ("Beer", "5C") | countOfBeer≥1 | testOperation1 |
| P2 | 5C=T, Beer=T, count>0=F | ("Beer", "5C") | countOfBeer=0 | testOperation2 |
| P3 | 5C=T, Beer=F, OJ=T, count>0=T | ("OrangeJuice", "5C") | countOfOrangeJuice≥1 | testOperation3 |
| P4 | 5C=T, Beer=F, OJ=T, count>0=F | ("OrangeJuice", "5C") | countOfOrangeJuice=0 | testOperation4 |
| P5 | 5C=T, Beer=F, OJ=F | ("Cola", "5C") | 任意 | testOperation5 |
| P6 | 5C=F, 1D=T, count>0=T, Beer=T, count>0=T | ("Beer", "1D") | countOfFiveCents≥1, countOfBeer≥1 | testOperation6 |
| P7 | 5C=F, 1D=T, count>0=T, Beer=T, count>0=F | ("Beer", "1D") | countOfFiveCents≥1, countOfBeer=0 | testOperation7 |
| P8 | 5C=F, 1D=T, count>0=F | ("Beer", "1D") | countOfFiveCents=0 | testOperation11 |
注意:testOperation8–10 覆盖的是 P6/P7 的变体(OJ 替代 Beer),属于路径等价类,不新增独立路径;testOperation12 覆盖
money非法值,属于边界值测试,不在基本路径范畴内。实验报告中列出的 12 个用例,实际只有 8 个承载基本路径覆盖使命。
3. JUnit 4.10 在 Eclipse 中的实战配置与测试用例工程化
3.1 Eclipse 环境下 JUnit 4.10 的零配置集成(避坑版)
虽然现代 IDE 多预装 JUnit,但本实验明确要求 v4.10 且需手动引入,这是为了暴露 ClassLoader 和依赖冲突的真实问题。以下是经 Eclipse 2021-06 验证的步骤(适配所有 3.7+ 版本):
步骤 1:下载并校验 JUnit 4.10 JAR
# 官方归档地址(非 Maven Central,因 v4.10 已下线) wget https://github.com/junit-team/junit4/releases/download/r4.10/junit-4.10.jar sha256sum junit-4.10.jar # 应输出:e4f1194e7b0c5d0f0e1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4提示:若下载链接失效,可从实验包附录中提取
junit-4.10.jar,用jar -tf junit-4.10.jar | head -n 5检查是否含org/junit/Assert.class。
步骤 2:Eclipse 中添加外部 JAR(关键操作)
- 右键项目 →Properties→Java Build Path→Libraries标签页
- 点击Add External JARs…→ 选择
junit-4.10.jar - 切勿点击 “Add Library…” → “JUnit” → “JUnit 4”,此操作会引入 IDE 内置版本(常为 4.13+),导致
TestCase类签名不兼容(v4.10 使用junit.framework.TestCase,新版用org.junit.Test) - 在Order and Export标签页,勾选
junit-4.10.jar,确保其在编译路径顶端
步骤 3:验证 JUnit 运行环境
新建测试类TestJUnitEnv.java:
// java import junit.framework.TestCase; public class TestJUnitEnv extends TestCase { public void testJUnitVersion() { // JUnit 4.10 的标志性行为:TestCase 类存在且无 @Test 注解 assertTrue(true); } }右键 →Run As→JUnit Test。若出现绿色进度条且显示 “testJUnitVersion: OK”,则环境就绪;若报错ClassNotFoundException: org.junit.runner.JUnitCore,说明误用了新版 JUnit。
3.2 测试用例设计:从路径映射到断言的精确构造
JUnit 4.10 的TestCase继承模式要求每个测试方法名以test开头,且必须public void。关键在于:预期结果字符串必须与operation()方法实际输出完全一致(含换行符、空格、大小写)。例如 P1 路径的断言:
// java public void testOperation1() { SaleMachine sm = new SaleMachine(); // 初始化默认库存 String expected = "Input Information \n" + "Type: Beer; Money: 5 Cents; Change: 0\n\n" + "Current State\n" + "Beer: 5\n" + "Orange Juice: 6\n" + "5 Cents: 7\n" + "1 Dollar: 6"; assertEquals(expected, sm.operation("Beer", "5C")); }参数说明:
expected字符串中的\n是 Unix 换行符(LF),Windows 环境下 Eclipse 默认使用 CRLF(\r\n),会导致断言失败- 解决方案:在 Eclipse 中设置全局换行符为 LF:Window → Preferences → General → Workspace→ 勾选"New text file line delimiter: Unix"
assertEquals()比较的是字符串字面量,非正则或模糊匹配;任何空格、标点差异都会失败
为什么不用@Test注解?
JUnit 4.10 支持两种风格:继承TestCase(本实验要求)或使用@Test(需import org.junit.Test)。混用会导致No tests found错误。实验报告中所有用例均基于继承模式,故必须严格遵循extends TestCase。
3.3 运行与调试:定位图二中 6 个错误的实操路径
当运行全部测试用例时,Eclipse JUnit 视图显示 6 个失败(Failures),对应图二。此时需逐个调试失败用例:
- 右键
testOperation11→Debug As → JUnit Test - 在
operation()方法入口设断点(line 36) - 按 F5 进入方法,观察变量
money="1D"→ 执行else if(money.equalsIgnoreCase("1D"))(line 62) - 下一步执行
if(countOfFiveCents > 0)(line 63),此时countOfFiveCents=0→ 走else分支(line 89) - 但原代码中该
else块缺失,直接跳到末尾return resultOfDeal;(line 91),而resultOfDeal未初始化 → 抛出NullPointerException
修复代码(在 line 89 后插入):
// java else { resultOfDeal = "Failure Information \n" + "Change Shortage"; return resultOfDeal; }同理,testOperation6失败是因为countOfFiveCents--后未检查是否为负数,需在 line 68 和 line 74 后添加:
// java if (countOfFiveCents < 0) { countOfFiveCents = 0; // 防止库存为负 }提示:Eclipse 的Outline 视图可快速定位所有
test*方法;Problems 视图会高亮显示未处理的NullPointerException和ArrayIndexOutOfBoundsException,比等待测试失败更高效。
4. 基本路径测试与 JUnit 的协同验证:用覆盖率工具反向检验路径完整性
4.1 EclEmma 插件安装与分支覆盖率分析
基本路径测试的终极验证不是看测试通过,而是看分支覆盖率是否达到 100%。EclEmma 是 Eclipse 官方推荐的免费覆盖率工具:
- Install:Help → Eclipse Marketplace → 搜索 “EclEmma” → Install(需重启 Eclipse)
- Run Coverage:右键测试类
TestSaleMachine→Coverage As → JUnit Test - 查看报告:底部
Coverage视图中,展开first.SaleMachine→operation()方法
此时会看到类似以下输出:
operation() [8/8 branches covered] Line 37: 2/2 (if money... ) Line 39: 2/2 (if type... ) Line 41: 2/2 (if countOfBeer... ) Line 51: 2/2 (if countOfOrangeJuice... ) Line 63: 2/2 (if countOfFiveCents... ) Line 65: 2/2 (if type && count... ) Line 71: 2/2 (if type && count... ) Line 89: 1/2 ← 缺失!第 89 行显示1/2,表明countOfFiveCents > 0的false分支未被执行——这正是 P8 路径缺失的证据。此时立即补上testOperation11并重跑覆盖率,即可看到8/8。
4.2 用 JUnit 断言验证路径执行痕迹(进阶技巧)
单纯依赖覆盖率工具可能掩盖逻辑错误(如某路径执行了,但返回了错误结果)。更可靠的方法是在operation()中埋点,让每个路径返回唯一标识:
// java // 在 operation() 开头添加 private static int pathId = 0; public String operation(String type, String money) { pathId++; // 每次调用递增 // ... 原逻辑 ... // 在每个 return 前添加: // System.out.println("Path ID: " + pathId); // 仅调试用 }然后在测试用例中捕获标准输出:
// java public void testOperation11() { SaleMachine sm = new SaleMachine(0,6,6,6); // 5C=0 // 重定向 stdout ByteArrayOutputStream outContent = new ByteArrayOutputStream(); System.setOut(new PrintStream(outContent)); sm.operation("Beer", "1D"); String output = outContent.toString().trim(); assertTrue(output.contains("Path ID: 8")); // P8 路径应输出 ID=8 }此技巧可确保:不仅路径被执行,而且执行的是你期望的那一条。
4.3 遗留系统单元测试的现实约束与应对策略
本实验用的是 JUnit 4.10,但现实中大量 Java 项目仍在维护 JUnit 3/4(尤其金融、电信等强稳定性领域)。面对此类legacyagent junit测试场景,需注意:
- ClassLoader 隔离:若项目同时引用 JUnit 4.10 和 Mockito 1.x,需确保
junit-4.10.jar在 build path 顶部,避免NoSuchMethodError - 无参构造函数强制:
TestCase子类必须有 public no-arg constructor,否则Class.newInstance()失败 - Setup/Teardown 替代方案:JUnit 4.10 不支持
@Before/@After,改用setUp()/tearDown()方法(需声明为protected void)
例如为每个测试复位库存:
// java protected void setUp() { // 每个 test* 方法执行前调用 // 不要在此处 new SaleMachine,因测试方法内需控制初始状态 } protected void tearDown() { // 清理资源 }最后一行技术内容:当
SaleMachine类被 Spring 管理时,需用@RunWith(SpringJUnit4ClassRunner.class),但此注解在 JUnit 4.10 中不可用——此时唯一解法是升级 JUnit 至 4.12+,或改用SpringJUnit4ClassRunner的兼容版 JAR(需手动下载spring-test-4.3.30.RELEASE.jar并添加到 build path)。
本文还有配套的精品资源,点击获取