第四章涵盖Spock
大体结构和所有细节,提供完美的注解支持、可读性增强。
这里以第六章电子商城为例并使用Spock
测试。
异常
之前的Spock
测试,期望结果都是一些断言和对象验证。但是有些情况下,期望结果是”异常“。比如你正开发一款库,你想抛出异常并通过Spock
验证异常。下面代码示例展示如何捕获异常。
抛出异常
def "Error conditions for unknown products"() {
given: "a warehouse"
WarehouseInventory inventory = new WarehouseInventory()
when: "warehouse is queried for the wrong product"
inventory.isProductAvailable("productThatDoesNotExist",1)
then: "an exception should be thrown"
thrown(IllegalArgumentException)
}
当Warehouse
根据name
没有找到product
时抛出异常,when
块没有抛出异常测试便会失败。IllegalArgumentException
异常也会被其他方法抛出,建议新建自定义异常(ProductNotFoundException
)代替。异常捕获之后也可以添加断言来严格约束,如下示例
其他异常约束
def "Error conditions for unknown products - better"() {
given: "a warehouse"
WarehouseInventory inventory = new WarehouseInventory()
when: "warehouse is queried for the wrong product"
inventory.isProductAvailable("productThatDoesNotExist",1)
then: "an exception should be thrown"
IllegalArgumentException e = thrown()
e.getMessage() == "Unknown product productThatDoesNotExist"
}
then
块期望抛出IllegalArgumentException
并且异常的message
是Unknown product productThatDoesNotExist
。你可以使用自定义的异常验证自定义字段,不必局限于内置异常。
不希望方法抛出异常的then
块大同小异:
不抛出异常
def "Negative quantity is the same as 0"() {
given: "a warehouse"
WarehouseInventory inventory = new WarehouseInventory()
and: "a product"
Product tv = new Product(name:"bravia",price:1200,weight:18)
when: "warehouse is loaded with a negative value"
inventory.preload(tv,-5)
then: "the stock is empty for that product"
notThrown(IllegalArgumentException) //断言没有抛出IllegalArgumentException
!inventory.isProductAvailable(tv.getName(),1)
}
关联Issue
在第四章中使用了@Subject
、@Title
和@Narrative
注解标注元数据,增加了对非技术人员的适应性,在报告文档中也有一定作用。
在一些大型企业应用里可能存在Issue
跟踪系统(如禅道),记录bug
和功能特性。Spock
提供@Issue
注解映射,如下。
@Issue
@Issue("JIRA-561") //标注测试的Issue
def "Error conditions for unknown products"() {
given: "a warehouse"
WarehouseInventory inventory = new WarehouseInventory()
when: "warehouse is queried for the wrong product"
inventory.isProductAvailable("productThatDoesNotExist",1)
then: "an exception should be thrown"
thrown(IllegalArgumentException)
}
@Issue("JIRA-561")
并没有自动映射到外部JIRA,只是文本而已。@Issue
参数也可以URL:
URL
@Issue("http://redmine.example.com/issues/2554")//URL
def "Error conditions for unknown products - better"() {
given: "a warehouse"
WarehouseInventory inventory = new WarehouseInventory()
when: "warehouse is queried for the wrong product"
inventory.isProductAvailable("productThatDoesNotExist",1)
then: "an exception should be thrown"
IllegalArgumentException e = thrown()
e.getMessage() == "Uknown product productThatDoesNotExist"
}
数组
@Issue(["JIRA-453","JIRA-678","JIRA-3485"])//数组
def "Negative quantity is the same as 0"() {
given: "a warehouse"
WarehouseInventory inventory = new WarehouseInventory()
and: "a product"
Product tv = new Product(name:"bravia",price:1200,weight:18)
when: "warehouse is loaded with a negative value"
inventory.preload(tv,-5)
then: "the stock is empty for that product"
notThrown(IllegalArgumentException)
!inventory.isProductAvailable(tv.getName(),1)
}
**@Issue**
也可以在**TDD**
(测试驱动开发)中用来标注要实现的功能。
超时
第七章中集成和功能测试的执行时间比较长,因为需要连接外部系统,比如数据库、web service
。包含其中的单元测试超时,需要及时反馈。Spock
提供@Timeout
注解支持。
@Timeout
@Timeout(5)//限制5秒完成
def "credit card charge happy path"() {
given: "a basket, a customer and a TV"
Product tv = new Product(name:"bravia",price:1200,weight:18)
BillableBasket basket = new BillableBasket()
Customer customer = new
Customer(name:"John",vip:false,creditCard:"testCard")
and: "a credit card service"
CreditCardProcessor creditCardSevice = new CreditCardProcessor()//外部系统
basket.setCreditCardProcessor(creditCardSevice)
when: "user checks out the tv"
basket.addProduct tv
boolean success = basket.checkout(customer)//需要连接web service
then: "credit card is charged"
success
}
@Timeout
快速定位集成测试中环境问题。外部系统出问题必然会超时,而普通单元测试运行极快。也可以调整超时参数。
自定义超时
@Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)//5000,单位毫秒
def "credit card charge happy path - alt "() {
given: "a basket, a customer and a TV"
Product tv = new Product(name:"bravia",price:1200,weight:18)
BillableBasket basket = new BillableBasket()
Customer customer = new
Customer(name:"John",vip:false,creditCard:"testCard")
and: "a credit card service"
CreditCardProcessor creditCardSevice = new CreditCardProcessor()
basket.setCreditCardProcessor(creditCardSevice)
when: "user checks out the tv"
basket.addProduct tv
boolean success = basket.checkout(customer)
then: "credit card is charged"
success
}
排除
大型项目有成千上万的单元测试,不太可能全都是激活状态。比如环境迁移、功能还没完成和需求未敲定等等,这些情况下测试可以跳过不执行。Spock
提供了几种方式实现。
排除单个测试:@Ignore
@Ignore
@Ignore("Until credit card server is migrated")//card service迁移完成之前跳过测试
def "credit card charge happy path"() {
given: "a basket, a customer and a TV"
Product tv = new Product(name:"bravia",price:1200,weight:18)
BillableBasket basket = new BillableBasket()
Customer customer = new
Customer(name:"John",vip:false,creditCard:"testCard")
and: "a credit card service"
CreditCardProcessor creditCardSevice = new CreditCardProcessor()
basket.setCreditCardProcessor(creditCardSevice)
when: "user checks out the tv"
basket.addProduct tv
boolean success = basket.checkout(customer)
then: "credit card is charged"
success
}
跳过测试使其他测试通过而不至于使构建失败。@Ignore
应该是一种临时解决方案,在后续开发中应调整。@Ignore
注解参数建议填写,增强可读性。**@Ignore**
类注解会跳过全部测试。
排除全部只留一个:@IgnoreRest
有时候集成测试失效,只有一个能正常运行。@IgnoreRest
排除失效,只允许单个。
@IgnoreRest
class KeepOneSpec extends spock.lang.Specification{
def "credit card charge - integration test"() { //web service 不可用,排除
[...code redacted for brevity...]
}
@IgnoreRest
def "credit card charge with mock"() { //mock 可以正常运行
[...code redacted for brevity...]
}
def "credit card charge no charge - integration test"() { //web service 不可用,排除
[...code redacted for brevity...]
}
}
运行结果:
只有@IgnoreRest
注解的方法运行。
这个注解比较特殊,你应该不会用到。
动态运行:@IgnoreIf
运行时环境
@Ignore
注解是静态的,有时候需要根据环境动态运行。Spock
提供@Ignore
注解套件实现动态运行。Spock
测试可以获取下列信息:
- 环境变量
- JVM系统参数
- 操作系统
环境变量、JVM参数、操作系统
class SimpleConditionalSpec extends spock.lang.Specification{
@IgnoreIf({ jvm.java9 }) //java9环境将忽略
def "credit card charge happy path"() {
[...code redacted for brevity...]
}
@IgnoreIf({ os.windows })//windwos环境将忽略
def "credit card charge happy path - alt"() {
[...code redacted for brevity...]
}
@IgnoreIf({ env.containsKey("SKIP_SPOCK_TESTS") }) //存在环境变量SKIP_SPOCK_TESTS将忽略
def "credit card charge happy path - alt 2"() {
[...code redacted for brevity...]
}
}
Windows
系统、JDK7
、无其他JVM
环境变量运行结果:
读者可根据自身情况自定义。
条件判断
@Ignore
注解还可自定义条件,支持闭包。闭包结果为false
时测试将被忽略。
@IgnoreIf
@IgnoreIf({ !new CreditCardProcessor().online() })//online()为true测试将忽略
def "credit card charge happy path - alt"() {
given: "a basket, a customer and a TV"
Product tv = new Product(name:"bravia",price:1200,weight:18)
BillableBasket basket = new BillableBasket()
Customer customer = new
Customer(name:"John",vip:false,creditCard:"testCard")
and: "a credit card service"
CreditCardProcessor creditCardSevice = new CreditCardProcessor()//外部依赖
basket.setCreditCardProcessor(creditCardSevice)
when: "user checks out the tv"
basket.addProduct tv
boolean success = basket.checkout(customer)//外部依赖
then: "credit card is charged"
success
}
布尔条件:@Requires
如果频繁使用@IgnoreIf
,可考虑使用@Requires
注解替代。
@Requires
@Requires({ new CreditCardProcessor().online() })//online()为false测试将被忽略
def "credit card charge happy path"() {
given: "a basket, a customer and a TV"
Product tv = new Product(name:"bravia",price:1200,weight:18)
BillableBasket basket = new BillableBasket()
Customer customer = new
Customer(name:"John",vip:false,creditCard:"testCard")
and: "a credit card service"
CreditCardProcessor creditCardSevice = new CreditCardProcessor()
basket.setCreditCardProcessor(creditCardSevice)
when: "user checks out the tv"
basket.addProduct tv
boolean success = basket.checkout(customer)
then: "credit card is charged"
success
}
@Requires
和@IgnoreIf
相反。闭包为true
将执行,反之。
自动资源清理:@AutoCleanup
第四章的cleanup
块可以实现资源清理,@AutoCleanup
注解是它的替代方式。
@AutoCleanup
@AutoCleanup("shutdown")//测试执行完成之后将自动调用CreditCardProcessor的shutdown方法
private CreditCardProcessor creditCardSevice = new CreditCardProcessor()
def "credit card connection is closed down in the end"() {
given: "a basket, a customer and a TV"
Product tv = new Product(name:"bravia",price:1200,weight:18)
BillableBasket basket = new BillableBasket()
Customer customer = new
Customer(name:"John",vip:false,creditCard:"testCard")
and: "a credit card service"
basket.setCreditCardProcessor(creditCardSevice)
when: "user checks out the tv"
basket.addProduct tv
boolean success = basket.checkout(customer)
then: "credit card is charged"
success
}
拥有@AutoCleanup
注解的资源,Spock
将自动调用它的close()
方法(测试失败依然调用)。close()
方法默认方法,允许自定义。cleanup
块和@AutoCleanup``3
注解读者可凭喜好选择使用。@AutoCleanup
也可配合@Shared
注解使用。