1. 概述
灰盒测试帮助我们在不穷举所有可能场景的情况下,构建足够的测试覆盖率。
本文将探讨这种方法,以及如何使用正交数组测试(OAT)技术来实践它。最后,我们将分析使用灰盒测试的优缺点。
2. 什么是灰盒测试?
首先,对比白盒测试与黑盒测试,再理解灰盒测试:
- 白盒测试:指对完全已知的算法部分进行测试。我们可以测试该算法的所有路径,因此会产生大量测试场景。
- 黑盒测试:仅测试应用的外部行为。不了解内部实现,难以覆盖所有路径,因此聚焦于有限数量的测试场景。
灰盒测试结合了两者特点:
- 利用白盒测试中的有限信息(如部分内部逻辑)
- 采用黑盒测试技术生成测试场景
✅ 核心优势:比白盒测试场景更少,但比黑盒测试覆盖更多功能。本质上,它是黑盒测试技术与白盒测试知识的混合体。
3. 实践灰盒测试
本节将通过佣金计算器演示应用,使用OAT技术实践灰盒测试。
3.1. 创建被测系统
先创建一个计算销售员平均佣金的应用,基于四个属性:
- 销售员等级(Level):L1、L2、L3
- 合同类型(Type):全职佣金制、合同工、自由职业者
- 资历(Seniority):初级、中级、高级
- 销售影响力(SalesImpact):低、中、高
实现SalaryCommissionPercentageCalculator
类:
public class SalaryCommissionPercentageCalculator {
public BigDecimal calculate(Level level, Type type,
Seniority seniority, SalesImpact impact) {
return BigDecimal.valueOf(DoubleStream.of(
level.getBonus(),
type.getBonus(),
seniority.getBonus(),
impact.getBonus(),
type.getBonus())
.average()
.orElse(0))
.setScale(2, RoundingMode.CEILING);
}
public enum Level {
L1(0.06), L2(0.12), L3(0.2);
private double bonus;
Level(double bonus) {
this.bonus = bonus;
}
public double getBonus() {
return bonus;
}
}
public enum Type {
FULL_TIME_COMMISSIONED(0.18), CONTRACTOR(0.1), FREELANCER(0.06);
// bonus字段、构造方法和getter
}
public enum Seniority {
JR(0.8), MID(0.13), SR(0.19);
// bonus字段、构造方法和getter
}
public enum SalesImpact {
LOW(0.06), MEDIUM(0.12), HIGH(0.2);
// bonus字段、构造方法和getter
}
}
代码说明:
- 四个枚举映射销售员属性,每个包含
bonus
字段表示佣金百分比 calculate()
方法使用原始流计算所有百分比的平均值- 通过
BigDecimal.setScale()
将结果四舍五入到两位小数
3.2. OAT技术简介
OAT技术基于田口玄一博士提出的田口设计实验。核心思想:
- 仅测试输入变量组合的子集
- 聚焦双因素交互,忽略重复交互
- 确保每个变量值与其他变量值仅交互一次
正交数组表示为val^var
:
val
:变量取值数量var
:输入变量数量
本例中:4个变量,每个3个取值 → 3^4
数组(田口设计中的"L9: 3-level 4-factor")
3.3. 获取正交数组
正交数组计算复杂,通常使用预定义数组。参考正交数组目录,选择L9 3-level 4-factor数组:
场景 # | var 1 | var 2 | var 3 | var 4 |
---|---|---|---|---|
1 | val 1 | val 1 | val 1 | val 1 |
2 | val 1 | val 2 | val 3 | val 2 |
3 | val 1 | val 3 | val 2 | val 3 |
4 | val 2 | val 1 | val 3 | val 3 |
5 | val 2 | val 2 | val 2 | val 1 |
6 | var 2 | val 3 | val 1 | val 2 |
7 | val 3 | val 1 | val 2 | val 2 |
8 | val 3 | val 2 | val 1 | val 3 |
9 | val 3 | val 3 | val 3 | val 1 |
⚠️ 关键特性:任意两个变量值组合仅出现一次(如var1=val1
和var2=val1
仅出现在场景1)
3.4. 映射变量与取值
按代码中枚举定义顺序映射变量:
var 1
→Level
(val 1 = L1, val 2 = L2, val 3 = L3)var 2
→Type
(val 1 = FULL_TIME_COMMISSIONED, ...)var 3
→Seniority
var 4
→SalesImpact
填充后的测试场景表:
场景 # | Level | Type | Seniority | SalesImpact |
---|---|---|---|---|
1 | L1 | FULL_TIME_COMMISSIONED | JR | LOW |
2 | L1 | CONTRACTOR | SR | MEDIUM |
3 | L1 | FREELANCER | MID | HIGH |
4 | L2 | FULL_TIME_COMMISSIONED | SR | HIGH |
5 | L2 | CONTRACTOR | MID | LOW |
6 | L2 | FREELANCER | JR | MEDIUM |
7 | L3 | FULL_TIME_COMMISSIONED | MID | MEDIUM |
8 | L3 | CONTRACTOR | JR | HIGH |
9 | L3 | FREELANCER | SR | LOW |
每行对应一个测试场景的输入组合。
3.5. 配置JUnit 5
为简单演示,使用JUnit 5进行单元测试。添加依赖到pom.xml
:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
3.6. 创建测试类
定义SalaryCommissionPercentageCalculatorUnitTest
:
class SalaryCommissionPercentageCalculatorUnitTest {
private SalaryCommissionPercentageCalculator testTarget = new SalaryCommissionPercentageCalculator();
@ParameterizedTest
@MethodSource("provideReferenceTestScenarioTable")
void givenReferenceTable_whenCalculateAverageCommission_thenReturnExpectedResult(Level level,
Type type, Seniority seniority, SalesImpact impact, double expected) {
BigDecimal got = testTarget.calculate(level, type, seniority, impact);
assertEquals(BigDecimal.valueOf(expected), got);
}
private static Stream<Arguments> provideReferenceTestScenarioTable() {
return Stream.of(
Arguments.of(L1, FULL_TIME_COMMISSIONED, JR, LOW, 0.26),
Arguments.of(L1, CONTRACTOR, SR, MEDIUM, 0.12),
Arguments.of(L1, FREELANCER, MID, HIGH, 0.11),
Arguments.of(L2, FULL_TIME_COMMISSIONED, SR, HIGH, 0.18),
Arguments.of(L2, CONTRACTOR, MID, LOW, 0.11),
Arguments.of(L2, FREELANCER, JR, MEDIUM, 0.24),
Arguments.of(L3, FULL_TIME_COMMISSIONED, MID, MEDIUM, 0.17),
Arguments.of(L3, CONTRACTOR, JR, HIGH, 0.28),
Arguments.of(L3, FREELANCER, SR, LOW, 0.12)
);
}
}
代码解析:
- 使用
@ParameterizedTest
和@MethodSource
参数化测试 provideReferenceTestScenarioTable()
提供正交数组中的测试数据- 每个
Arguments.of()
对应一个测试场景及预期结果 - 通过
assertEquals
验证实际结果与预期值
4. 灰盒测试的优缺点
✅ 优势
- 大幅减少测试场景:本例中输入组合从81个降至9个,同时保持良好覆盖率
- 避免组合爆炸:对于10变量×10取值的系统(10^10种组合),OAT使测试可行
- 提升效率:改善测试代码可维护性和开发速度
❌ 劣势
- 覆盖不完整:无法覆盖所有输入排列,可能遗漏关键场景或边界情况
- 场景选择风险:依赖正交数组的科学性,不当使用可能导致测试盲区
踩坑提示:OAT虽能减少测试量,但对复杂交互逻辑仍需补充专项测试!
5. 总结
本文通过OAT技术实践了灰盒测试,显著减少了测试场景数量。但需谨慎评估使用场景,避免遗漏重要边界情况。
经验之谈:灰盒测试是平衡效率与覆盖率的利器,但别把它当作万能钥匙——关键业务逻辑仍需白盒测试把关。
完整示例代码见GitHub仓库。