ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基本路径测试:用圈复杂度锁定最小独立路径集

基本路径测试:用圈复杂度锁定最小独立路径集 简介本资源是一份面向软件工程专业本科生及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决策节点1money.equalsIgnoreCase(5C)line 37→ 两个出口true / false决策节点25C分支内type.equals(typeOfGoods[0])line 39→ true / false决策节点35CBeer分支内countOfBeer0line 41→ true / false决策节点45COrangeJuice分支内countOfOrangeJuice 0line 51→ true / false决策节点51D分支内countOfFiveCents 0line 63→ true / false决策节点61D有零钱分支内type.equals(typeOfGoods[0])countOfBeer0line 65→ true / false决策节点71D有零钱非Beer分支内type.equals(typeOfGoods[1])countOfOrangeJuice0line 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 2PE边数N节点数P连通分量数但在实际编码中我们用更可靠的两种方式交叉验证方法一判定节点计数法最常用V(G) 判定节点数 1本方法中判定节点共 7 个见 2.1 节故V(G) 7 1 8方法二区域计数法画图验证在控制流图中独立闭合区域即“环”数量即为圈复杂度。从 CFG 可数出5C → Beer → count0形成 1 个区域5C → Beer → count0形成 1 个区域5C → OrangeJuice → count0形成 1 个区域5C → OrangeJuice → count0形成 1 个区域1D → count0 → Beer → count0形成 1 个区域1D → count0 → Beer → count0形成 1 个区域1D → count0 → OrangeJuice → count0形成 1 个区域1D → count0 → OrangeJuice → count0形成 1 个区域1D → count0形成 1 个区域注意此区域独立于上述 8 个→ 共 9 个区域矛盾修正1D → count0是单边路径不构成闭合区域而1D → count0 → Type Error是另一条终止路径需合并到已有区域。最终确认V(G) 8与判定节点法一致。方法三代码行级验证防漏判使用 Eclipse 的Coverage Tool需安装 EclEmma运行全部 JUnit 用例查看operation()方法的分支覆盖率Branch Coverage。若显示为 100%则说明 8 条路径已全被覆盖若低于 100%则缺失路径对应未被执行的if/else组合。2.3 从圈复杂度导出 8 条基本路径及对应测试输入路径编号按深度优先顺序生成每条路径必须包含唯一新增的边即该路径使总边覆盖数增加路径编号覆盖的判定序列Ttrue, Ffalse输入 (type, money)关键状态约束对应 JUnit 用例P15CT, BeerT, count0T(Beer, 5C)countOfBeer≥1testOperation1P25CT, BeerT, count0F(Beer, 5C)countOfBeer0testOperation2P35CT, BeerF, OJT, count0T(OrangeJuice, 5C)countOfOrangeJuice≥1testOperation3P45CT, BeerF, OJT, count0F(OrangeJuice, 5C)countOfOrangeJuice0testOperation4P55CT, BeerF, OJF(Cola, 5C)任意testOperation5P65CF, 1DT, count0T, BeerT, count0T(Beer, 1D)countOfFiveCents≥1, countOfBeer≥1testOperation6P75CF, 1DT, count0T, BeerT, count0F(Beer, 1D)countOfFiveCents≥1, countOfBeer0testOperation7P85CF, 1DT, count0F(Beer, 1D)countOfFiveCents0testOperation11注意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。步骤 2Eclipse 中添加外部 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 换行符LFWindows 环境下 Eclipse 默认使用 CRLF\r\n会导致断言失败解决方案在 Eclipse 中设置全局换行符为 LFWindow → Preferences → General → Workspace→ 勾选New text file line delimiter: UnixassertEquals()比较的是字符串字面量非正则或模糊匹配任何空格、标点差异都会失败为什么不用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 进入方法观察变量money1D→ 执行else if(money.equalsIgnoreCase(1D))line 62下一步执行if(countOfFiveCents 0)line 63此时countOfFiveCents0→ 走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 官方推荐的免费覆盖率工具InstallHelp → Eclipse Marketplace → 搜索 “EclEmma” → Install需重启 EclipseRun 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); // 5C0 // 重定向 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 路径应输出 ID8 }此技巧可确保不仅路径被执行而且执行的是你期望的那一条。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。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进