第四章涵盖Spock大体结构和所有细节,提供完美的注解支持、可读性增强。
这里以第六章电子商城为例并使用Spock测试。

异常

之前的Spock测试,期望结果都是一些断言和对象验证。但是有些情况下,期望结果是”异常“。比如你正开发一款库,你想抛出异常并通过
Spock验证异常。下面代码示例展示如何捕获异常。

抛出异常

  1. def "Error conditions for unknown products"() {
  2. given: "a warehouse"
  3. WarehouseInventory inventory = new WarehouseInventory()
  4. when: "warehouse is queried for the wrong product"
  5. inventory.isProductAvailable("productThatDoesNotExist",1)
  6. then: "an exception should be thrown"
  7. thrown(IllegalArgumentException)
  8. }

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并且异常的messageUnknown 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)

}

notThrown()可读性确实不错。

关联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:

数组
@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

}

**@Timeout**主要还是用来探测环境问题。

排除

大型项目有成千上万的单元测试,不太可能全都是激活状态。比如环境迁移、功能还没完成和需求未敲定等等,这些情况下测试可以跳过不执行。Spock提供了几种方式实现。

排除单个测试:@Ignore

@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...]
 }

}

运行结果:
image.png
只有@IgnoreRest注解的方法运行。
这个注解比较特殊,你应该不会用到。

动态运行:@IgnoreIf

运行时环境

@Ignore注解是静态的,有时候需要根据环境动态运行。Spock提供@Ignore注解套件实现动态运行。Spock测试可以获取下列信息:

  • 环境变量
  • JVM系统参数
  • 操作系统

Spock可以根据上述信息动态选择运行。

环境变量、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环境变量运行结果:
image.png
读者可根据自身情况自定义。

条件判断

@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

}

online()发送ping包检测外部依赖的状况。

布尔条件:@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注解使用。