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=val1var2=val1仅出现在场景1)

3.4. 映射变量与取值

按代码中枚举定义顺序映射变量:

  • var 1Level(val 1 = L1, val 2 = L2, val 3 = L3)
  • var 2Type(val 1 = FULL_TIME_COMMISSIONED, ...)
  • var 3Seniority
  • var 4SalesImpact

填充后的测试场景表:

场景 # 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仓库


原始标题:Gray Box Testing Using the OAT Technique