1. 引言

Spock 是一个优秀的测试框架,尤其在提升测试覆盖率方面表现出色。本文将深入探讨 Spock 的数据管道(Data Pipes)机制,展示如何通过向数据管道添加额外数据来提升代码的行覆盖率和分支覆盖率。同时也会介绍当测试数据量过大时的处理方案。

2. 测试对象

我们从一个带特殊逻辑的加法方法开始:如果任一输入参数为 42,则直接返回 42:

public class DataPipesSubject {
    int addWithATwist(final int first, final int second) {
        if (first == 42 || second == 42) {
            return 42;
        }
        return first + second;
    }
}

我们需要用多种输入组合测试这个方法。接下来将演示如何编写并演进一个简单的数据驱动测试,通过数据管道提供输入数据。

3. 准备数据驱动测试

首先创建测试类,从单场景测试开始,逐步添加数据管道功能:

@Title("数据管道多种用法测试")
class DataPipesTest extends Specification {
    @Subject
    def dataPipesSubject = new DataPipesSubject()
    // ...
}

✅ 使用了 Spock 的 @Title 注解为测试类提供上下文信息
✅ 使用 @Subject 注解标记被测对象(注意导入 spock.lang.Subject 而非 javax.security.auth.Subject

现在用 given/when/then 语法编写基础测试(输入 1 和 2):

def "给定两个数字,执行加法时返回两数之和"() {
    given: "输入参数"
    def first = 1
    def second = 2

    and: "预期结果"
    def expectedResult = 3

    when: "执行加法"
    def result = dataPipesSubject.addWithATwist(first, second)

    then: "验证结果"
    result == expectedResult
}

为引入数据管道,将输入数据从 given/and 块移至 where 块:

def "使用 where 子句提供输入数据,执行加法时返回两数之和"() {
    when: "执行加法"
    def result = dataPipesSubject.addWithATwist(first, second)

    then: "验证结果"
    result == expectedResult

    where: "多组输入数据"
    first = 1
    second = 2
    expectedResult = 3
}

⚠️ Spock 会将 where 块中的变量隐式转换为测试方法参数,相当于:

def "使用 where 子句提供输入数据..."(int first, int second, int expectedResult)

简化测试结构,合并 when/thenexpect 块:

def "使用 expect 块简化测试,执行加法时返回两数之和"() {
    expect: "验证加法结果"
    dataPipesSubject.addWithATwist(first, second) == expectedResult

    where: "多组输入数据"
    first = 1
    second = 2
    expectedResult = 3
}

现在测试已简化,可以添加第一个数据管道。

4. 什么是数据管道?

Spock 的数据管道是向测试注入不同数据组合的机制,当需要测试多个场景时能保持代码可读性。
管道可以是任何 Iterable 对象,甚至可以自定义实现 Iterable 接口的类。

4.1 简单数据管道

数组天然支持 Iterable,将单组输入转为数组并用 << 操作符注入:

where: "多组输入数据"
first << [1]
second << [2]
expectedResult << [3]

通过扩展数组添加更多测试用例(2+2=4 和 3+5=8):

first << [1, 2, 3]
second << [2, 2, 5]
expectedResult << [3, 4, 8]

为提升可读性,合并 firstsecond 为多变量数组管道:

where: "多组输入数据"
[first, second] << [
    [1, 2],
    [2, 2],
    [3, 5]
]

and: "预期结果"
expectedResult << [3, 4, 8]

✅ 可引用已定义的数据管道,例如用 expectedResult = first + second 替代单独管道
❌ 但因被测方法有特殊逻辑(42 的处理),仍需显式提供预期结果:

[first, second, expectedResult] << [
    [1, 2, 3],
    [2, 2, 4],
    [3, 5, 8]
]

4.2 Map 和方法数据源

在 Spock 2.2+ 中,可用 Map 作为数据源:

where: "Map 形式的输入数据"
[first, second, expectedResult] << [
    [
        first : 1,
        second: 2,
        expectedResult: 3
    ],
    [
        first : 2,
        second: 2, 
        expectedResult: 4
    ]
]

也可从独立方法获取数据:

[first, second, expectedResult] << dataFeed()

dataFeed 方法实现示例:

def dataFeed() {
    [ 
        [
            first : 1,
            second: 2,
            expectedResult: 3
        ],
        [
            first : 2,
            second: 2,
            expectedResult: 4
        ]
    ]
}

⚠️ 多变量处理仍显繁琐,接下来用数据表优化。

5. 数据表

Spock 的数据表格式将多个数据管道整合为更直观的表格形式:

where: "多组输入数据"
first | second || expectedResult
1     | 2      || 3
2     | 2      || 4
3     | 5      || 8

✅ 每行代表一个测试场景,大幅提升可读性
✅ 使用双竖线 || 分隔输入与预期结果(最佳实践)

运行上述测试时,发现未覆盖所有代码行。addWithATwist 方法中 if 分支未被触发:

if (first == 42 || second == 42) {
    return 42;
}

添加 42 的测试用例覆盖所有分支:

42    | 10     || 42
1     | 42     || 42

最终数据表(实现行覆盖率和分支覆盖率):

where: "多组输入数据"
first | second || expectedResult
1     | 2      || 3
2     | 2      || 4
3     | 5      || 8
42    | 10     || 42
1     | 42     || 42

测试执行时,每个迭代会显示为独立用例:

DataPipesTest
 - use table to supply the inputs
    - use table to supply the inputs [first: 1, second: 2, expectedResult: 3, #0]
    - use table to supply the inputs [first: 2, second: 2, expectedResult: 4, #1]
...

6. 可读性优化技巧

6.1 在方法名中插入变量

通过在方法名中插入变量使测试输出更直观,使用 # 前缀引用列名:

def "给定 #scenario 场景,输入 #first 和 #second 时返回 #expectedResult"() {
    expect: "验证加法结果"
    dataPipesSubject.addWithATwist(first, second) == expectedResult

    where: "多组输入数据"
    scenario       | first | second || expectedResult
    "常规场景"     | 1     | 2      || 3
    "双2场景"      | 2     | 2      || 4
    "特殊值场景"   | 42    | 10     || 42
}

测试输出示例:

DataPipesTest
- 给定 #scenario 场景,输入 #first 和 #second 时返回 #expectedResult
  - 给定 常规场景 场景,输入 1 和 2 时返回 3
  - 给定 双2场景 场景,输入 2 和 2 时返回 4
...

❌ 变量名错误时 Spock 会报错:Error in @Unroll, could not find a matching variable for expression: myWrongVariableName
✅ 可在同一行引用已定义的列:

scenario              | first | second || expectedResult
"引用场景"            | 2     | first  || first + second

6.2 处理过宽的表格列

IDE 通常支持 Spock 表格格式化(如 IntelliJ 的 Ctrl+Alt+L),但长字符串仍会导致表格过宽。示例方法:

String addExclamation(final String first) {
    return first + '!';
}

长字符串测试用例:

def "当表格过长时,使用静态或共享变量缩短表格"() {
    expect: "验证结果"
    dataPipesSubject.addExclamation(longString) == expectedResult

    where: "长字符串输入"
    longString                                                                                                  || expectedResult
    '当字符串过长时,可以使用静态或 @Shared 变量提升表格可读性' || '当字符串过长时,可以使用静态或 @Shared 变量提升表格可读性!'
}

static@Shared 变量优化(表格仅支持这三类变量):

static def STATIC_VARIABLE = '当字符串过长时,可以使用静态变量'
@Shared
def SHARED_VARIABLE = '当字符串过长时,可以使用 @Shared 注解变量'
...
scenario         | longString      || expectedResult
'使用静态变量'    | STATIC_VARIABLE || "$STATIC_VARIABLE!"
'使用@Shared变量'| SHARED_VARIABLE || "$SHARED_VARIABLE!"

✅ 使用 Groovy 字符串插值("$变量名")提升可读性
✅ 复杂表达式需用 ${} 包裹(如 ${first + second}

用双下划线 __ 分割大型表格

where: "分割的大型表格"
first | second
1     | 2
2     | 3
3     | 5
__
expectedResult | _
3              | _
5              | _
8              | _

⚠️ 分割后各部分行数必须一致
⚠️ 单列表格需添加空列 _ 满足至少两列的要求

6.3 替代分隔符

可用分号 ; 替代竖线 | 作为分隔符:

first ; second ;; expectedResult
1     ; 2      ;; 3
2     ; 3      ;; 5
3     ; 5      ;; 8

❌ 同一表格中不能混用 |;

7. 总结

本文深入探讨了 Spock 的数据驱动测试机制:

  • ✅ 通过 where 块实现数据管道
  • ✅ 使用数据表提升可读性
  • ✅ 通过添加测试用例提升代码覆盖率
  • ✅ 处理大型表格的多种优化技巧

完整代码示例可在 GitHub 获取。


原始标题:Improving Test Coverage and Readability With Spock’s Data Pipes and Tables | Baeldung