1. 概述

在本篇文章中,我们将深入探讨 Spock 提供的扩展机制。

在编写测试时,我们常常需要对测试生命周期进行定制或增强,比如:

  • 条件执行某些测试
  • 对不稳定集成测试自动重试
  • 控制测试执行顺序
  • 设置超时机制
  • 标记待实现功能的测试

Spock 提供了丰富的扩展机制,允许我们通过注解或配置文件的方式对测试行为进行灵活控制。

接下来,我们将介绍 Spock 中最常用的扩展用法。

2. Maven 依赖

在开始前,我们需要引入必要的 Maven 依赖项:

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <version>1.3-groovy-2.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.7</version>
    <scope>test</scope>
</dependency>

✅ 确保这些依赖已正确添加到你的 pom.xml 文件中。

3. 基于注解的扩展

Spock 的大多数扩展功能都是通过注解实现的。我们可以将这些注解添加到测试类或方法上,以控制测试行为。

3.1. @Ignore

当我们需要临时跳过某些测试时,可以使用 @Ignore 注解。

@Ignore
def "I won't be executed"() {
    expect:
    true
}

Spock 不会执行该测试方法,大多数 IDE 会将其标记为 skipped

也可以在类级别使用:

@Ignore
class IgnoreTest extends Specification

还可以添加忽略原因:

@Ignore("probably no longer needed")

3.2. @IgnoreRest

如果你想只执行某个测试方法,其余都跳过,可以使用 @IgnoreRest

def "I won't run"() { }

@IgnoreRest
def 'I will run'() { }

def "I won't run too"() { }

⚠️ 注意:这个注解非常有用,尤其是在调试时只运行一个测试方法。

3.3. @IgnoreIf

有时候我们希望根据特定条件跳过测试,比如只在某个操作系统上跳过:

@IgnoreIf({System.getProperty("os.name").contains("windows")})
def "I won't run on windows"() { }

Spock 提供了一些辅助类,简化条件判断:

  • os:操作系统信息(spock.util.environment.OperatingSystem
  • jvm:JVM 信息
  • sys:系统属性
  • env:环境变量

例如使用 os.isWindows()

@IgnoreIf({ os.isWindows() })
def "I'm using Spock helper classes to run only on windows"() {}

3.4. @Requires

有时候我们更倾向于表达“在什么条件下才执行”,而不是“在什么条件下不执行”。这时可以使用 @Requires

@Requires({ os.isWindows() })
def "I will run only on Windows"()

@IgnoreIf 相比,@Requires 更加语义清晰。

3.5. @PendingFeature

在 TDD 中,我们常常先写测试,再实现功能。此时可以使用 @PendingFeature 标记尚未完成的测试:

@PendingFeature
def 'test for not implemented yet feature. Maybe in the future it will pass'()

区别于 @Ignore@PendingFeature 会执行测试,但忽略失败。如果测试通过了,会报错提醒你移除注解。

3.6. @Stepwise

默认情况下,测试方法是无序执行的。如果需要按顺序执行,可以使用 @Stepwise

@Stepwise
class StepwiseTest extends Specification {

    def 'I will run as first'() { }

    def 'I will run as second'() { }
}

⚠️ 警告:测试应尽量独立,避免依赖顺序。但若确实需要,此注解是不错的选择。

3.7. @Timeout

设置测试方法的执行时间限制:

@Timeout(1)
def 'I have one second to finish'() { }

默认单位是秒,也可以指定其他单位:

@Timeout(value = 200, unit = TimeUnit.MILLISECONDS)
def 'I will fail after 200 millis'() { }

类级别使用时,对所有方法生效:

@Timeout(5)
class ExampleTest extends Specification {

    @Timeout(1)
    def 'I have one second to finish'() {}

    def 'I will have 5 seconds timeout'() {}
}

方法级别的注解优先级高于类级别。

3.8. @Retry

对于不稳定的集成测试,可以使用 @Retry 自动重试:

@Retry
def 'I will retry three times'() { }

默认重试 3 次。也可以指定重试条件:

  • 重试特定异常:
@Retry(exceptions = [RuntimeException])
def 'I will retry only on RuntimeException'() { }
  • 重试特定异常信息:
@Retry(condition = { failure.message.contains('error') })
def 'I will retry with a specific message'() { }
  • 带延迟重试:
@Retry(delay = 1000)
def 'I will retry after 1000 millis'() { }

也可以在类级别使用。

3.9. @RestoreSystemProperties

在测试中修改系统属性后,可以使用此注解自动恢复:

@RestoreSystemProperties
def 'all environment variables will be saved before execution and restored after tests'() {
    given:
    System.setProperty('os.name', 'Mac OS')
}

⚠️ 注意:修改系统属性会影响并发测试,应避免在并发环境下使用。

3.10. 人性化的标题与描述

使用 @Title 添加可读性强的测试类标题:

@Title("This title is easy to read for humans")
class CustomTitleTest extends Specification

使用 @Narrative 添加多行描述:

@Narrative("""
    as a user
    i want to save favourite items 
    and then get the list of them
""")
class NarrativeDescriptionTest extends Specification

3.11. @See

为测试方法添加外部参考链接:

@See("https://example.org")
def 'Look at the reference'()

多个链接用列表形式:

@See(["https://example.org/first", "https://example.org/second"])
def 'Look at the references'()

3.12. @Issue

标记测试方法关联的 Issue:

@Issue("https://jira.org/issues/LO-531")
def 'single issue'() {}

@Issue(["https://jira.org/issues/LO-531", "http://jira.org/issues/LO-123"])
def 'multiple issues'()

3.13. @Subject

标记被测试的类:

@Subject
ItemService itemService // 初始化代码...

目前仅用于文档说明。

4. 配置扩展

除了注解,我们还可以通过 SpockConfig.groovy 文件全局配置扩展行为。

通常将其放在 src/test/resources/SpockConfig.groovy,Spock 会自动读取。

4.1. 控制堆栈信息输出

默认情况下,Spock 会过滤堆栈信息:

runner {
    filterStackTrace true
}

关闭后,会显示完整堆栈:

runner {
    filterStackTrace false
}

✅ 小贴士:CI 环境建议关闭过滤,本地调试开启更清爽。

4.2. 条件配置

我们可以根据环境变量动态配置:

if (System.getenv("FILTER_STACKTRACE") == null) {   
    filterStackTrace false
}

4.3. @Issue 的 URL 前缀

统一配置 @Issue 的 URL 前缀:

report {
    issueNamePrefix 'Bug '
    issueUrlPrefix 'https://jira.org/issues/'
}

这样 @Issue("LO-531") 会被解析为 https://jira.org/issues/LO-531

4.4. 优化测试执行顺序

启用测试执行顺序优化,让失败的测试优先执行:

runner {
  optimizeRunOrder true
}

该功能默认关闭。

4.5. 排除或包含测试类/方法

通过配置文件排除特定测试类或带有特定注解的方法:

import extensions.TimeoutTest
import spock.lang.Issue

runner {
    exclude {
        baseClass TimeoutTest
        annotation Issue
    }
}

也可以使用 include 包含特定测试。

4.6. 生成测试报告

启用测试报告生成:

report {
    enabled true
    logFileDir '.'
    logFileName 'report.json'
    logFileSuffix new Date().format('yyyy-MM-dd')
}

报告包含 @Title, @See, @Issue, @Narrative 等注解信息。

也可以通过系统属性控制:

-Dspock.report.enabled=true
-Dspock.logFileDir=.
-Dspock.logFileName=report.json

5. 总结

Spock 的扩展机制非常强大且灵活,涵盖了测试生命周期的各个方面:

  • 通过注解控制测试行为
  • 通过配置文件统一管理扩展行为
  • 支持测试报告生成、执行顺序优化、条件执行、自动重试等高级功能

熟练掌握这些扩展,能极大提升测试效率和可维护性。

完整示例代码可在 GitHub 项目 中找到。


原始标题:Guide to Spock Extensions