软件测试中的圈复杂度及其示例

McCabe 的圈复杂度是什么?
软件测试中的循环复杂度 是用于测量软件程序复杂度的测试指标。它是软件程序源代码中独立路径的定量度量。循环复杂度可以通过使用控制流图或针对软件程序中的函数、模块、方法或类来计算。
独立路径定义为至少有一条边在任何其他路径中都未遍历过的路径。
该度量标准由 Thomas J. McCabe 于 1976 年开发,它基于程序的控制流表示。控制流将程序描述为由节点和边组成的图。
在图中,节点表示处理任务,而边表示节点之间的控制流。
程序流程图表示法
程序的流程图表示法定义了通过边连接的多个节点。以下是 if-else、While、until 等语句的流程图以及流程的正常顺序。
如何计算圈复杂度
数学表示:
从数学角度来看,它是图论中所有独立路径的集合。 Code 程序的复杂度可以用以下公式定义——
V(G) = E - N + 2
哪里,
E – 边数
N – 节点数
V (G) = P + 1
其中 P = 谓词节点的数量(包含条件的节点)
示例–
i = 0; n=4; //N-Number of nodes present in the graph while (i<n-1) do j = i + 1; while (j<n) do if A[i]<A[j] then swap(A[i], A[j]); end do; j=j+1; end do;
该程序的流程图如下
通过数学计算,
- V(G) = 9 – 7 + 2 = 4
- V(G) = 3 + 1 = 4 (条件节点为1,2、3和XNUMX节点)
基组四个独立的执行路径:
- 1,7
- 1,2,6,1,7
- 1,2,3,4,5,2,6,1,7
- 1,2,3,5,2,6,1,7
环路复杂性的性质
以下是圈复杂度的属性:
- V(G) 是图中独立路径的最大数量
- 电压(G)>=1
- 如果 V (G) = 1,则 G 有一条路径
- 常用的指导原则是,单个模块的 V(G) 应保持在 10 或以下。
该指标对软件测试有何用处
基础路径测试是白盒测试技术之一,它保证在测试过程中至少执行一条语句。它检查程序中每一条线性无关的路径,这意味着…… 所需的测试用例数量等于程序的圈复杂度。.
这个度量之所以有用,是因为循环复杂度(M)的特性——
- M 可以是实现分支覆盖的测试用例数量(上限)
- M 可以是图中路径的数量。(下限)
考虑这个例子——
If (Condition 1) Statement 1 Else Statement 2 If (Condition 2) Statement 3 Else Statement 4
该程序的圈复杂度为8-7+2=3。
由于复杂度已计算为 3,因此对于上述示例,完成路径覆盖需要三个测试用例。
应遵循的步骤
计算圈复杂度和测试用例设计应遵循以下步骤。
第四步 – 使用代码中的节点和边构建图
第四步 – 识别独立路径
第四步 – 循环复杂度计算
第四步 – 测试用例设计
一旦基本集合形成, 测试用例 应写入以执行所有路径。
更多关于 V (G)
如果程序较小,可以手动计算循环复杂度。如果程序非常复杂,则需要使用自动化工具,因为这涉及更多流程图。根据复杂度数字,团队可以得出需要采取的措施。
下表概述了复杂度数 v (G) 及其对应含义:
| 复杂度数 | 意 |
|---|---|
| 1到10 |
结构化且编写良好的代码 高可测试性 成本和精力较少 |
| 11到20 |
复杂代码 中等可测试性 成本和精力中等 |
| 21到40 |
非常复杂的代码 可测试性低 成本和精力投入高 |
| > 40 |
根本无法测试 成本和精力非常高 |
计算圈复杂度的工具
有许多工具可用于确定应用程序的复杂性。一些复杂性计算工具用于特定技术。复杂性可以通过程序中的决策点数量来确定。决策点是源代码中的 if、for、for-each、while、do、catch、case 语句。
工具示例包括
- 欧克林特 – C 和相关语言的静态代码分析器
- SonarQube – 报告了超过 25 种语言的循环复杂性和认知复杂性
- Visual Studio Code 指标 – .NET 程序集的内置圈复杂度分析
- Radon 和 Lizard – 用于命令行复杂度分析的分析器 Python 以及分别针对多语言项目
- 指标 – 查找指标 Java 相关申请
环路复杂性的应用
圈复杂度可以证明在以下方面非常有用
- 帮助开发人员和测试人员确定独立的路径执行
- 开发人员可以保证所有路径都至少测试过一次。
- 帮助我们更加专注于未被发现的路径
- 提高代码覆盖率 软件工程
- 评估与应用程序或程序相关的风险
- 在周期早期使用这些指标可以降低项目的风险
如何降低圈复杂度
高复杂度值只是一个信号,而非最终结论。四次重构就能实现实践中大部分的复杂度降低。
- Extract 方法。 将分支移到单独的函数中,可以将复杂性分散到两个模块中。系统整体的复杂性不变,但每个单元都可以独立测试。
- 用查找表替换条件链。 冗长的 if-else-if 语句链测试同一个变量,最终会变成一个映射或开关,将多个决策点合并为一个。
- 使用保护性条款。 尽早返回无效输入可以消除单个大型 if-else 块创建的嵌套,而不会改变行为。
- 用多态性取代条件语句。 当条件判断基于类型时,将每个分支移到它自己的类中,就可以完全消除这种判断。
之前,V(G) = 4:
if (user != null) { if (user.isActive()) { if (user.hasRole("admin")) { return grantAccess(); } } } return denyAccess();
之后,行为保持不变,但嵌套结构被移除:
if (user == null) return denyAccess(); if (!user.isActive()) return denyAccess(); if (!user.hasRole("admin")) return denyAccess(); return grantAccess();
关于该指标的注意事项。 圈复杂度统计的是决策次数,而不是难度。一个包含 20 个简单 case 的 switch 语句得分为 21,但易于阅读;而一个深度嵌套的块得分为 8,可能更难理解。应该利用这个数值来寻找需要审查的代码,而不是将其作为作弊的目标。


.png)
.png)