1. 引言
本文将介绍 Spock 框架——一个基于 Groovy 的测试框架。Spock 的核心目标是利用 Groovy 的语言特性,成为传统 JUnit 测试栈的强力替代方案。
Groovy 是运行在 JVM 上的语言,能与 Java 无缝集成。除了互操作性,它还提供了动态类型、可选类型和元编程等额外语言特性。
通过 Groovy 的加持,Spock 为 Java 应用测试引入了全新的表达方式,这些在普通 Java 代码中难以实现。本文将通过实际案例探索 Spock 的核心概念。
2. Maven 依赖
开始前,先添加 Maven 依赖:
<properties>
<spock-core.version>2.4-M1-groovy-4.0</spock-core.version>
<groovy-all.version>4.0.16</groovy-all.version>
<gmavenplus-plugin.version>3.0.2</gmavenplus-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>${spock-core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy-all.version}</version>
<type>pom</type>
</dependency>
</dependencies>
⚠️ 注意:我们仅将 Spock 和 Groovy 用于测试,因此依赖范围设为 test
。
✅ 需要额外配置 gmavenplus
插件编译 Groovy 代码,并调整 surefire
插件识别 Groovy 测试文件:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>${gmavenplus-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compileTests</goal>
</goals>
</execution>
</executions>
<configuration>
<testSources>
<testSource>
<directory>${project.basedir}/src/test/groovy</directory>
<includes>
<include>**/*.groovy</include>
</includes>
</testSource>
</testSources>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<testSourceDirectory>src/test/groovy</testSourceDirectory>
<includes>
<include>**/*Specification.groovy</include>
<include>**/*Test.groovy</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
现在可以编写第一个 Spock 测试了(Groovy 代码)。记住:测试文件需放在 src/test/groovy
目录而非 src/test/java
。
3. Spock 测试结构
3.1 规约(Specifications)与特性(Features)
在 src/test/groovy
目录创建第一个测试文件 FirstSpecification.groovy
:
class FirstSpecification extends Specification {
}
✅ 所有 Spock 测试类必须继承 Specification
接口。现在实现第一个特性(即测试方法):
def "one plus one should equal two"() {
expect:
1 + 1 == 2
}
关键点解析:
- 方法名:直接用字符串描述(比 JUnit 的驼峰命名更直观)
- 代码块:测试逻辑放在
expect
块中(后续详解) - 隐式断言:无需显式调用断言方法,表达式结果为
true
即通过
⚠️ 在 Spock 中,特性(feature) 等同于 JUnit 的测试用例(test case)。
3.2 代码块(Blocks)
JUnit 中常通过注释划分测试阶段(如 given/when/then),而 Spock 提供原生支持:
@Test
public void givenTwoAndTwo_whenAdding_thenResultIsFour() {
// Given
int first = 2;
int second = 2;
// When
int result = first + second;
// Then
assertTrue(result == 4)
}
Spock 的代码块标签:
| 块名 | 作用 |
|------------|----------------------------------------------------------------------|
| setup
/given
| 初始化测试环境(隐式块,未指定块时默认加入此块) |
| when
| 触发被测方法(提供刺激) |
| then
| 验证结果(断言) |
| expect
| 合并 when
和 then
(适用于简单场景) |
| cleanup
| 清理资源(如删除临时文件/数据库数据) |
用代码块重构测试:
def "two plus two should equal four"() {
given:
int left = 2
int right = 2
when:
int result = left + right
then:
result == 4
}
✅ 代码块让测试逻辑更清晰易读。
3.3 利用 Groovy 特性增强断言
在 then
和 expect
块中,断言是隐式的——每个表达式都会被求值,结果为 false
时测试失败。结合 Groovy 特性可大幅简化代码:
def "Should be able to remove from list"() {
given:
def list = [1, 2, 3, 4] // Groovy 简化列表创建
when:
list.remove(0)
then:
list == [2, 3, 4] // 操作符重载调用 equals() 方法
}
❌ 故意触发失败的错误信息示例:
Condition not satisfied:
list == [1, 3, 4]
| |
| false
[2, 3, 4]
<Click to see difference>
at FirstSpecification.Should be able to remove from list(FirstSpecification.groovy:30)
✅ Spock 会智能分析失败断言,提供详细调试信息。
分组断言(Same Target Object)
Spock 2.0+ 支持用 with
块对同一对象分组断言(灵感来自 Groovy 的 Object.with
):
class ShoppingCartTest extends Specification {
def "verify multiple properties of a ShoppingCart"() {
given:
ShoppingCart cart = new ShoppingCart()
cart.addItem("Apple", 3)
cart.addItem("Banana", 2)
expect:
with(cart) { // 所有断言针对 cart 对象
totalItems == 5
totalPrice == 10.00
items.contains("Apple")
items.contains("Banana")
}
}
}
3.4 异常断言
Spock 提供原生异常验证,无需 try-catch 或第三方库:
def "Should get an index out of bounds when removing a non-existent item"() {
given:
def list = [1, 2, 3, 4]
when:
list.remove(20) // 触发异常
then:
thrown(IndexOutOfBoundsException) // 验证异常类型
list.size() == 4 // 测试继续执行
}
✅ thrown()
会验证异常类型但不会中断测试。
4. 数据驱动测试
4.1 什么是数据驱动测试?
数据驱动测试:用不同参数重复测试同一行为(如数学运算)。在 Java 中通常称为参数化测试(parameterized testing)。
4.2 JUnit 参数化测试实现
先看 JUnit 的传统实现(对比用):
@RunWith(Parameterized.class)
public class FibonacciTest {
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 1, 1 }, { 2, 4 }, { 3, 9 }
});
}
private int input;
private int expected;
public FibonacciTest (int input, int expected) {
this.input = input;
this.expected = expected;
}
@Test
public void test() {
assertEquals(fExpected, Math.pow(3, 2)); // 注意:此处代码有误,仅为示例
}
}
❌ 缺点:代码冗余,可读性差,需创建二维数组和包装类。
4.3 Spock 数据表(Datatables)
Spock 用 数据表 简化参数化测试:
def "numbers to the power of two"(int a, int b, int c) {
expect:
Math.pow(a, b) == c
where:
a | b | c
1 | 2 | 1
2 | 2 | 4
3 | 2 | 9
}
✅ 优势:
- 参数表与测试逻辑紧邻
- 无需样板代码
- 方法名和块结构清晰表达意图
4.4 数据表失败场景
当测试失败时,Spock 会精确定位问题数据行:
Condition not satisfied:
Math.pow(a, b) == c
| | | | |
4.0 2 2 | 1
false
Expected :1
Actual :4.0
5. Mocking
5.1 什么是 Mocking?
Mocking:替换被测系统依赖类的行为(如将网络调用替换为模拟对象)。核心目标是隔离业务逻辑测试。
5.2 Spock Mocking
Spock 内置 Mock 框架,利用 Groovy 动态特性简化操作:
PaymentGateway paymentGateway = Mock() // 类型推断
// 或显式指定类型
def paymentGateway = Mock(PaymentGateway)
✅ 默认采用 宽松 Mock(lenient mocking):未定义的方法返回合理默认值(而非抛异常),使测试更健壮。
when:
def result = paymentGateway.makePayment(12.99)
then:
result == false // 默认返回 false
5.3 方法桩(Stubbing)
配置 Mock 方法对特定参数的响应:
given:
paymentGateway.makePayment(20) >> true // 参数 20 返回 true
when:
def result = paymentGateway.makePayment(20)
then:
result == true
高级桩示例:
| 语法 | 作用 |
|-----------------------|--------------------------|
| makePayment(_) >> true
| 忽略参数,始终返回 true |
| makePayment(_) >>> [true, true, false]
| 按顺序返回列表中的值 |
5.4 改进的 Mock 声明(Spock 2.0+)
可在创建 Mock 时直接定义交互,提升可读性:
class BookServiceTest extends Specification {
def "should retrieve book details and verify method calls"() {
given:
// 创建 Mock 并定义行为
def bookRepository = Mock(BookRepository) {
findById(1L) >> new Book("Effective Java", "Joshua Bloch")
findById(2L) >> null
}
def bookService = new BookService(bookRepository)
when:
Book effectiveJava = bookService.getBookDetails(1L)
Book unknownBook = bookService.getBookDetails(2L)
then:
1 * bookRepository.findById(1L) // 验证调用次数
1 * bookRepository.findById(2L)
effectiveJava.title == "Effective Java"
unknownBook == null
}
}
5.5 验证(Verification)
验证 Mock 方法是否按预期调用(尤其对返回 void
的方法):
def "Should verify notify was called"() {
given:
def notifier = Mock(Notifier)
when:
notifier.notify('foo')
then:
1 * notifier.notify('foo') // 验证调用次数
}
❌ 故意设置错误期望(调用 2 次)的错误信息:
Too few invocations for:
2 * notifier.notify('foo') (1 invocation)
灵活验证语法:
| 语法 | 作用 |
|--------------------------|-------------------------------|
| 2 * notifier.notify(_)
| 忽略参数,验证调用 2 次 |
| 2 * notifier.notify(!'foo')
| 验证参数不为 'foo' 的调用 |
5.6 全局 Mock(Global Mocks)
Spock 2.0+ 支持全局替换类实例,便于测试无法注入 Mock 的场景:
public class UtilityClass {
public static String getMessage() {
return "Original Message";
}
}
public class MessageService {
public String fetchMessage() {
return UtilityClass.getMessage(); // 直接调用静态方法
}
}
测试代码:
class MessageServiceTest extends Specification {
def "should use global mock for UtilityClass"() {
given:
// 创建全局 Mock
def utilityMock = GroovySpy(UtilityClass, global: true)
utilityMock.getMessage() >> "Mocked Message"
when:
MessageService service = new MessageService()
String message = service.fetchMessage()
then:
1 * utilityMock.getMessage() // 验证调用
message == "Mocked Message" // 验证结果
}
}
✅ 无需修改被测代码即可替换依赖行为。
6. 总结
本文快速介绍了 Spock 测试框架的核心能力:
- ✅ 利用 Groovy 特性编写更表达性强的测试
- ✅ 通过规约(Specifications)和特性(Features)组织测试
- ✅ 简单粗暴的数据驱动测试(数据表)
- ✅ 原生支持 Mocking 和断言
完整示例代码可在 GitHub 获取(Maven 项目可直接运行)。