1. 引言
编写测试时,我们经常需要断言对象的多个属性。Spock框架提供了一些实用的语言特性,帮助我们消除对象比较时的重复代码。
本教程将学习如何使用Spock的辅助方法with()
和verifyAll()
重构测试代码,使测试更易读。
2. 准备工作
首先创建一个Account
类,包含账户名、当前余额和透支限额:
public class Account {
private String accountName;
private BigDecimal currentBalance;
private long overdraftLimit;
// getters and setters
}
3. 基础测试
创建AccountTest
规范类,测试账户属性的getter/setter。使用@Subject
注解标记测试对象,每个测试前创建新账户:
class AccountTest extends Specification {
@Subject
Account account = new Account()
def "设置账户属性后,获取值应与设置值一致"() {
when: "设置账户属性"
account.setAccountName("My Account")
account.setCurrentBalance(BigDecimal.TEN)
account.setOverdraftLimit(0)
then: "获取的值应与设置值一致"
account.getAccountName() == "My Account"
account.getCurrentBalance() == BigDecimal.TEN
account.getOverdraftLimit() == 0
}
}
这里在断言部分重复引用了account
对象。属性越多,重复代码越多。
4. 重构方案
让我们重构代码以减少重复。
4.1. 断言陷阱
初步尝试可能是提取getter比较到单独方法:
void verifyAccount(Account accountToVerify) {
accountToVerify.getAccountName() == "My Account"
accountToVerify.getCurrentBalance() == BigDecimal.TEN
accountToVerify.getOverdraftLimit() == 0
}
但这里有个坑!创建一个使用他人账户值的验证方法:
void verifyAccountRefactoringTrap(Account accountToVerify) {
accountToVerify.getAccountName() == "Someone else's account"
accountToVerify.getCurrentBalance() == BigDecimal.ZERO
accountToVerify.getOverdraftLimit() == 9999
}
在then
块中调用该方法:
then: "获取的值应与设置值一致"
verifyAccountRefactoringTrap(account)
运行测试,即使值不匹配也会通过!为什么?
Spock的隐式断言只在测试方法中生效,不会在调用方法中自动执行。
如何解决?
4.2. 方法内显式断言
将比较逻辑移到单独方法时,必须手动添加assert
关键字。
创建显式断言的验证方法:
void verifyAccountAsserted(Account accountToVerify) {
assert accountToVerify.getAccountName() == "My Account"
assert accountToVerify.getCurrentBalance() == BigDecimal.TEN
assert accountToVerify.getOverdraftLimit() == 0
}
在then
块中调用:
then: "获取的值应与设置值一致"
verifyAccountAsserted(account)
测试通过,修改断言值会正确失败。
4.3. 返回布尔值
另一种方法是返回布尔结果。使用Groovy的&&
操作符组合断言:
boolean matchesAccount(Account accountToVerify) {
accountToVerify.getAccountName() == "My Account"
&& accountToVerify.getCurrentBalance() == BigDecimal.TEN
&& accountToVerify.getOverdraftLimit() == 0
}
测试在所有条件匹配时通过,但有个缺点:失败时无法知道是哪个条件不满足。
虽然这些方法可行,但Spock的辅助方法提供了更优雅的解决方案。
5. 辅助方法
Spock提供了with
和verifyAll
两个辅助方法,能更优雅地解决问题!两者工作方式类似,先学习with
。
5.1. with()
辅助方法
with()
接收一个对象和该对象的闭包。传递对象给with()
后,对象的属性和方法会被添加到上下文中,在闭包内无需前缀对象名。
重构方法使用with
:
void verifyAccountWith(Account accountToVerify) {
with(accountToVerify) {
getAccountName() == "My Account"
getCurrentBalance() == BigDecimal.TEN
getOverdraftLimit() == 0
}
}
**注意:Spock辅助方法内自动应用power assertions,即使在不同方法中也不需要assert
**。
**通常不需要单独方法,直接在测试断言中使用with()
**:
then: "获取的值应与设置值一致"
with(account) {
getAccountName() == "My Account"
getCurrentBalance() == BigDecimal.TEN
getOverdraftLimit() == 0
}
调用方法方式:
then: "获取的值应与设置值一致"
verifyAccountWith(account)
with()
方法遇到第一个不匹配的比较就会失败测试。
5.2. 用于Mock对象
with
也可用于断言Mock交互。创建Mock Account并调用setter:
given: '一个mock账户'
Account mockAccount = Mock()
when: "调用setter方法"
mockAccount.setAccountName("A Name")
mockAccount.setOverdraftLimit(0)
验证时,在with
作用域内可省略mockAccount
前缀。
5.3. verifyAll()
辅助方法
有时希望测试失败时看到所有断言结果。这时可用verifyAll()
,用法与with
相同:
verifyAll(accountToVerify) {
getAccountName() == "My Account"
getCurrentBalance() == BigDecimal.TEN
getOverdraftLimit() == 0
}
verifyAll()
遇到失败时会继续执行,报告作用域内所有失败的比较。
5.4. 嵌套辅助方法
当对象包含嵌套对象时,可嵌套使用辅助方法。
先创建Address
类并添加到Account
:
public class Address {
String street;
String city;
// getters and setters
}
public class Account {
private Address address;
// getter and setter and rest of class
}
在测试中创建地址对象:
given: "一个地址"
Address myAddress = new Address()
def myStreet = "1, The Place"
def myCity = "My City"
myAddress.setStreet(myStreet)
myAddress.setCity(myCity)
添加到账户:
when: "设置账户属性"
account.setAddress(myAddress)
account.setAccountName("My Account")
比较地址时,基础方式:
account.getAddress().getStreet() == myStreet
account.getAddress().getCity() == myCity
可提取变量改进:
def address = account.getAddress()
address.getCity() == myCity
address.getStreet() == myStreet
但更优方案是使用with
:
with(account.getAddress()) {
getStreet() == myStreet
getCity() == myCity
}
嵌套到账户比较中。with(account)
将账户加入作用域,可省略前缀:
then: "获取的值应与设置值一致"
with(account) {
getAccountName() == "My Account"
with(getAddress()) {
getStreet() == myStreet
getCity() == myCity
}
}
Groovy支持属性直接访问,可用属性名替代getter:
with(account) {
accountName == "My Account"
with(address) {
street == myStreet
city == myCity
}
}
6. 工作原理
了解了with()
和verifyAll()
如何提升可读性,但它们如何工作?
查看with()
方法签名:
with(Object, Closure)
可传递两个参数使用:
with(account, (acct) -> {
acct.getAccountName() == "My Account"
acct.getOverdraftLimit() == 0
})
Groovy对最后一个参数是Closure的方法有特殊语法支持,允许将闭包声明在括号外:
with(account) {
getAccountName() == "My Account"
getOverdraftLimit() == 0
}
注意测试中只传递了一个参数account
,第二个参数是紧随with
的花括号内的代码块,该闭包接收第一个参数account
作为操作对象。
7. 总结
本教程学习了如何使用Spock的with()
和verifyAll()
辅助方法减少对象比较时的样板代码。掌握了:
- ✅ 简单对象和复杂嵌套对象的使用方式
- ✅ Groovy属性记法使断言更简洁
- ✅ 与Mock对象的配合使用
- ✅ 利用Groovy闭包语法提升可读性
完整代码示例可在GitHub仓库查看。