AETG 算法 - 组合测试中的优化
Intro
假设一个待测试的系统,有 k 个输入/系统参数,每个参数有 Vi (1<=i<=k) 个情况,那么覆盖所有情况的测试组数就来到了所有 Vi 的乘积,这个结果就非常糟糕了。
有没有这样一种可能,我们只需要测其中的一小部分组合,就能测出大部分的错误呢?
单缺陷假设
失效极少是由两个(或多个)缺陷的同时发生引起的。
Kuhn 和 Reilly 分析了 Mozilla 浏览器的 错误报告记录,发现:
超过 70% 的错误是由 2 个以内的参数相互作用触发的;
超过 90% 的错误是由 3 个以内的参数相互作用触发的。
换言之,当我们的测试用例保证:
覆盖任意 2 个变量的所有等价类组合,我们就能避免 70% 的错误;
覆盖任意 3 个变量的所有等价类组合,我们就能避免 90% 的错误。
T-way Coverage
由单缺陷假设
引出,覆盖 t 个变量的所有等价类组合而进行的测试用例设计,我们称之为 t-way coverage
。
我们只需要找到一批测试组,他们覆盖了所有 t 个变量的等价类组合,当 t >= 2,就能避免大部分的错误,无疑是实惠的选择,并且后面的结果中我们将看到,这让需要测试的组数大大地减少。当然,缺陷就是如果有错误由 n > t 个变量的特定值组合触发时,我们就检查不到了。
基于这个思想的组合生成算法有很多:
Greedy Algorithms:
- AETG - Automatic Efficient Test Case Generator
- TCG - Test Case Generator
- DDA - Deterministic Density Algorithm
- IPO - In Parameter Order
Heuristic/Meta-heuristic Search:
- Hill Climbing
- Simulated Annealing
- Genetic Algorithms
- Tabu Search
下面详细介绍一下 AETG 算法并实现它。
AETG
为生成一个 Cover Array,也就是包括所有 T 个变量的等价类组合的用例集,假设系统总共有 K 个参数,T 即为这个 Cover Array 的强度,遵循以下步骤:
每个参数有有限的多个可能的取值。假设一开始有未覆盖对 Uncovered Pairs(以下简称 ucps)集和覆盖对 Covered Pairs(以下简称 cps)集。 ucps 初期即为所有 T 个变量的等价类组合,而 cps 初期为空。
注意,pair 看起来像是一个二元的概念,但这里也包括 T > 2 的场景,也称其为 pair。
按以下方法生成新的测试用例:
- 设定一个候选组数量,记为 M,因为下面的过程会用到随机数,所以我们每次生成一个正式的测试用例加入 Cover Array 前,都先生成 M 个候选测试用例,在其中选择效果最好的。候选测试用例生成过程如下:
- 一开始候选测试用例为空,为了确定第一个参数和它的取值,遍历每一个参数的所有可能取值,选择在未覆盖对集中出现频率最高的那一个参数和它的取值,将其确定并纳入候选测试用例中。
- 对于之后的参数进行确定。当还有参数没有确定时,假设已经确定了 t - 1 个参数,接下来随机从未确定的参数中选取一个记为 kt,此时分两种情况:
- t <= T: 此时候选测试组的参数数量还达不到指定的强度。遍历该参数的所有可能取值,将每个取值和已经确定的所有参数进行组合,此时的组合仍然比一个 ucps 还小。遍历所有 ucps ,得出包含这个组合的 ucps 数量。选取包含这个组合的 ucps 数量最大的情况下,该参数的取值,作为该参数的实际值。
- t > T: 此时候选测试组的参数数量达到了指定的强度。遍历该参数的所有可能取值,对于每一个可能取值,需要从 t - 1 个已经确定的参数中选择 T - 1 个,和当前取值进行组合,得到一个大小和 ucps 相当的组合,判断是否是 ucps 之一。由于有(从 t-1 中选 T-1)个选取方案,将覆盖的 ucps 加起来得到总数。选取 ucps 总数最大的情况下,该参数的取值,作为该参数的实际值。
- 对于这 M 个候选的测试用例,选择其中覆盖了新的 ucps 数量最多的那个,作为真实的测试用例加入到 Cover Array 中去。
- 设定一个候选组数量,记为 M,因为下面的过程会用到随机数,所以我们每次生成一个正式的测试用例加入 Cover Array 前,都先生成 M 个候选测试用例,在其中选择效果最好的。候选测试用例生成过程如下:
重复上述生成测试用例的过程,直到 ucps 为空,即实现 T-way Coverage。
实现上的细节
测试用例和 ucps 的数据形式
我使用 [4, 3, 0, 2, 1]
这样的列表来表示一个 5 个参数的测试用例,其中的值 a 表示该参数取其可能的取值中第 a + 1 项(例如 0 表示取这个参数的第 1 个可能取值)。
使用 [-1, -1, 0, -1, 1]
这样的列表来表示一个 2 元的变量覆盖对,其中的 -1 表示该参数未被选择。
当存在重复的局部最优时,随机选择
因为 AETG 本身是一个贪心的算法,上述过程中也可以看出它有好几个取局部最优的过程。尤其在上述的算法描述中 t <= T
这个过程中,t == T
时,会发现,因为此时无论是选取哪个参数取值,最优情况只有 刚好是一个 ucps
,也就是说最多只被 1 个 ucps 包含。此时会出现大量的重复的局部最优解,然而在全局情况下,这些局部最优解有着巨大的性能差异,所以我们不能使用:
1 | if ucps_count > max_ucps_count: |
这种形式来取值,导致在同样的情况下,永远只会取最早出现的那个值,略过了后续的重复的局部最优解。
所以我做了这样的优化,当遇到重复的局部最优解时,将其加入一个列表,最后使用 random.choice(best_param_values)
来随机选取其一。因为 AETG 中,是从 M 个由随机过程生成的候选测试用例中选取最优来作为真实的测试用例,当 M 这个数量足够时,我们就能依靠随机选取重复的局部最优的贪心来更加接近全局的最优。
这个处理的效果是非常显著的,生成的结果对比如下:
未采用随机选择重复的局部最优:
品牌 | 能效等级 | 支持IPv6 | 类型 | 处理器 | 厚度 | 机身材质 | 内存容量 | 屏幕尺寸 | Pairs |
---|---|---|---|---|---|---|---|---|---|
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 18.4英寸 | 36 |
mechrevo | 五级能效 | yes | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 17.3英寸 | 15 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 复合材质 | 128GB | 17英寸 | 15 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 金属+复合材质 | 128GB | 16.0-16.9英寸 | 15 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 金属 | 128GB | 15.0-15.9英寸 | 15 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 18.1-20.0mm | 含碳纤维 | 128GB | 14.0-14.9英寸 | 15 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 15.1-18.0mm | 含碳纤维 | 128GB | 13.0-13.9英寸 | 15 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 15.0mm 及以下 | 含碳纤维 | 128GB | 13.0英寸以下 | 15 |
mechrevo | 三级能效 | no | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 64GB | 18.4英寸 | 15 |
mechrevo | 二级能效 | no | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 18.4英寸 | 8 |
mechrevo | 一级能效 | no | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 18.4英寸 | 8 |
mechrevo | 五级能效 | no | 高端游戏本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 18.4英寸 | 8 |
mechrevo | 五级能效 | no | 高端轻薄本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 18.4英寸 | 8 |
mechrevo | 五级能效 | no | 游戏本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 18.4英寸 | 8 |
mechrevo | 五级能效 | no | 轻薄本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 18.4英寸 | 8 |
mi | 五级能效 | yes | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 18.4英寸 | 9 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 复合材质 | 64GB | 17.3英寸 | 5 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 至强 | 20.0mm 以上 | 金属+复合材质 | 128GB | 18.4英寸 | 9 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 金属 | 64GB | 17英寸 | 3 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 18.1-20.0mm | 含碳纤维 | 64GB | 16.0-16.9英寸 | 4 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 15.1-18.0mm | 含碳纤维 | 64GB | 15.0-15.9英寸 | 4 |
mechrevo | 五级能效 | no | 高性能轻薄本 | intel 奔腾 | 15.0mm 及以下 | 含碳纤维 | 64GB | 14.0-14.9英寸 | 3 |
acer | 五级能效 | yes | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 18.4英寸 | 8 |
mi | 三级能效 | no | 高性能轻薄本 | intel 奔腾 | 20.0mm 以上 | 含碳纤维 | 128GB | 18.4英寸 | 3 |
采用随机选择重复的局部最优:
品牌 | 能效等级 | 支持IPv6 | 类型 | 处理器 | 厚度 | 机身材质 | 内存容量 | 屏幕尺寸 | Pairs |
---|---|---|---|---|---|---|---|---|---|
acer | 三级能效 | no | 轻薄本 | Apple M2 | 18.1-20.0mm | 含碳纤维 | 24GB | 15.0-15.9英寸 | 36 |
haier | 一级能效 | yes | 高端轻薄本 | intel i5 | 18.1-20.0mm | 复合材质 | 32GB | 13.0英寸以下 | 36 |
dell | 二级能效 | yes | 高端游戏本 | AMD R7 | 15.0mm 及以下 | 金属+复合材质 | 36GB | 16.0-16.9英寸 | 36 |
lenovo | 一级能效 | no | 游戏本 | Apple M1 Max | 20.0mm 以上 | 金属 | 64GB | 17英寸 | 36 |
thinkpad | 三级能效 | yes | 游戏本 | 龙芯 | 15.1-18.0mm | 金属 | 48GB | 17.3英寸 | 35 |
haier | 五级能效 | no | 游戏本 | intel i3 | 15.0mm 及以下 | 复合材质 | 20GB | 18.4英寸 | 34 |
huawei | 五级能效 | yes | 高性能轻薄本 | AMD R3 | 20.0mm 以上 | 复合材质 | 16GB | 17英寸 | 33 |
apple | 一级能效 | yes | 轻薄本 | AMD R9 | 15.0mm 及以下 | 含碳纤维 | 4GB | 14.0-14.9英寸 | 33 |
hp | 一级能效 | no | 高性能轻薄本 | 兆芯 | 15.1-18.0mm | 金属 | 40GB | 13.0-13.9英寸 | 32 |
huawei | 五级能效 | no | 轻薄本 | AMD 速龙 | 15.1-18.0mm | 金属+复合材质 | 8GB | 13.0英寸以下 | 32 |
mi | 二级能效 | no | 高端游戏本 | intel 奔腾 | 15.1-18.0mm | 复合材质 | 4GB | 13.0-13.9英寸 | 31 |
mechrevo | 三级能效 | yes | 高端游戏本 | 麒麟 | 20.0mm 以上 | 金属+复合材质 | 32GB | 13.0-13.9英寸 | 29 |
honor | 五级能效 | no | 高端轻薄本 | AMD R9 | 20.0mm 以上 | 金属 | 128GB | 18.4英寸 | 29 |
asus | 五级能效 | yes | 高端轻薄本 | intel 奔腾 | 18.1-20.0mm | 金属+复合材质 | 40GB | 14.0-14.9英寸 | 28 |
acer | 二级能效 | yes | 游戏本 | Apple M1 Pro | 15.1-18.0mm | 含碳纤维 | 12GB | 13.0-13.9英寸 | 26 |
lenovo | 三级能效 | yes | 高端游戏本 | 高通 | 18.1-20.0mm | 含碳纤维 | 6GB | 18.4英寸 | 28 |
asus | 二级能效 | no | 高性能轻薄本 | intel 赛扬 | 18.1-20.0mm | 金属+复合材质 | 128GB | 17.3英寸 | 27 |
thinkpad | 一级能效 | yes | 高端游戏本 | intel i3 | 20.0mm 以上 | 含碳纤维 | 8GB | 16.0-16.9英寸 | 25 |
haier | 二级能效 | no | 高端轻薄本 | AMD R5 | 15.1-18.0mm | 金属 | 8GB | 15.0-15.9英寸 | 24 |
mechrevo | 三级能效 | no | 轻薄本 | intel 至强 | 15.1-18.0mm | 金属 | 12GB | 16.0-16.9英寸 | 25 |
mi | 二级能效 | yes | 高性能轻薄本 | intel i5 | 20.0mm 以上 | 含碳纤维 | 64GB | 17英寸 | 21 |
honor | 五级能效 | yes | 高性能轻薄本 | Apple M1 | 15.0mm 及以下 | 含碳纤维 | 6GB | 16.0-16.9英寸 | 22 |
dell | 三级能效 | no | 高端轻薄本 | intel 赛扬 | 15.1-18.0mm | 复合材质 | 24GB | 14.0-14.9英寸 | 24 |
apple | 五级能效 | no | 高端轻薄本 | 飞腾 | 18.1-20.0mm | 含碳纤维 | 48GB | 16.0-16.9英寸 | 23 |
我们可以观察到,不同测试用例间的参数取值重复的情况大大缓解,新覆盖的 ucps 数量的衰减慢了非常多,也就是每个用例可以新覆盖更多的 ucps,让总测试用例的数量大约又减少了 50% ~ 80%。
对于一个参数表为 [12, 4, 2, 5, 21, 4, 4, 13, 8]
(共 9 个参数,数组中的数为每个参数的可能的取值的个数)的组合测试问题,取其 2-way 覆盖解:
测试用例数 | |
---|---|
全组合测试 | 16773120 |
AETG(贪心未优化) | 1478 |
AETG(贪心优化) | 302 |
心得
由于涉及到大量的数组计算和变换,我的 Python 实现在面对大量组合尤其是 3-way 及以上时,速度惨不忍睹(因为我的数组变换是全 Python 手写的)。如果需要更加高效的程序,也许 numpy
或直接换一个语言是更好的选择。
参考链接
为什么需要组合覆盖 why t-way coverage - turtle_fly - 博客园 (cnblogs.com)
The Automatic Efficient Test Generator (AETG) system | IEEE Conference Publication | IEEE Xplore