本书中的例子并非贴合生产,大多例子都非常简单。企业应用的单元测试通常具有大量代码。即使是“纯粹”的单元测试,也有必不可少的准备工作。通过使用Groovy/Spock的with()方法可以解决一部分问题(第四章)。此节将更进一步,解决其他问题。
此例是银行贷款应用。且有5个主要类:
- 🍏
Custom.java - 🍐
Loan.java - 🍑
CreditCard.java - 🍒
ContactDetails.java - 
使用封装方法提高可读性
第四章讲述了保持
when:块精简的重要性,但是在大型应用里,长代码段不可避免:def "a bank customer with 3 credit cards is never given a loan"() {given: "a customer that wants to get a loan"Customer customer = new Customer(name:"John Doe")and: "his credit cards"//冗长的代码BankAccount account1 = new BankAccount()account1.with {setNumber("234234")setHolder("John doe")balance=30}CreditCard card1 = new CreditCard("447978956666")card1.with{setHolder("John Doe")assign(account1)}customer.owns(card1)BankAccount account2 = new BankAccount()account2.with{setNumber("3435676")setHolder("John Doe")balance=30}CreditCard card2 = new CreditCard("4443543354")card2.with{setHolder("John Doe")assign(account2)}customer.owns(card2)BankAccount account3 = new BankAccount()account2.with{setNumber("45465")setHolder("John Doe")balance=30}CreditCard card3 = new CreditCard("444455556666")card3.with{setHolder("John Doe")assign(account3)}customer.owns(card3)when:"a loan is requested"//简洁易读Loan loan = new Loan()customer.requests(loan)then: "loan should not be approved"//简洁易读!loan.approved}
👆虽然所有块都添加了描述,断言也很清晰。但是
setup阶段的冗长代码和业务关联不紧密,描述初始化信用卡,实际代码却创建信用卡和银行账户。即使有with方法加持,冗长代码还是难以阅读。比如30对贷款许可有何影响?这种情况下,有必要重构: ```groovy def “a bank customer with 3 credit cards is never given a loan -alt”() { given: “a customer that wants to get a loan” Customer customer = new Customer(name:”John Doe”)and: “his credit cards” customer.owns(createSampleCreditCard(“447978956666”,”John Doe”)) customer.owns(createSampleCreditCard(“4443543354”,”John Doe”)) customer.owns(createSampleCreditCard(“444455556666”,”John Doe”))
when:”a loan is requested” Loan loan = new Loan() customer.requests(loan)
then: “loan should not be approved” !loan.approved }
 
private CreditCard createSampleCreditCard(String number, String holder){ BankAccount account = new BankAccount() account.with{ setNumber(“45465”) setHolder(holder) balance=30 } CreditCard card = new CreditCard(number) card.with{ setHolder(holder) assign(account) } return card }
👆封装方法,提升阅读性:
- 🍈降低代码量
- 🍉明确含义
- 🍊隐藏信用卡初始化细节
- 🍋参数明确含义
甚至可以根据业务,进一步优化:
```groovy
def "a bank customer with 3 credit cards is never given a loan -alt 2"() {
    given: "a customer that wants to get a loan"
    String customerName ="doesNotMatter"//变量
    Customer customer = new Customer(name:customerName)
    and: "his credit cards"
    customer.owns(createSampleCreditCard("anything",customerName))
    customer.owns(createSampleCreditCard("whatever",customerName))
    customer.owns(createSampleCreditCard("notImportant",customerName))
    expect: "customer already has 3 cards"
    customer.getCards().size() == 3
    when:"a loan is requested"
    Loan loan = new Loan()
    customer.requests(loan)
    then: "therefore loan is not approved"
    !loan.approved
}
then:块中重用断言
因为技术原因,then:块中的断言无法像when:块中处理,需要特别处理,如:
def "Normal approval for a loan"() {
    given: "a bank customer"
    Customer customer = new Customer(name:"John Doe",city:"London",address:"10 Bakers",phone:"32434")
    and: "his/her need to buy a house "
    Loan loan = new Loan(years:5, amount:200.000)
    when:"a loan is requested"
    customer.requests(loan)
    then: "loan is approved as is"
    loan.approved
    loan.amount == 200.000
    loan.years == 5
    loan.instalments == 60
    loan.getContactDetails().getPhone() == "32434"//冗长的断言
    loan.getContactDetails().getAddress() == "10 Bakers"
    loan.getContactDetails().getCity() == "London"
    loan.getContactDetails().getName() == "John Doe"
    customer.activeLoans == 1
}
👆then:块中冗长的断言可以这样优化:
def "Normal approval for a loan - alt"() {
    given: "a bank customer"
    Customer customer = new Customer(name:"John Doe",city:"London",address:"10 Bakers",phone:"32434")
    and: "his/her need to buy a house "
    int sampleTimeSpan=5
    int sampleAmount = 200.000
    Loan loan = new Loan(years:sampleTimeSpan, amount:sampleAmount)
    when:"a loan is requested"
    customer.requests(loan)
    then: "loan is approved as is"
    with(loan) {
        approved
        amount == sampleAmount
        years == sampleTimeSpan
        installments == sampleTimeSpan * 12
    }
    customer.activeLoans == 1
    and: "contact details are kept or record"
    with(loan.contactDetails) {//字段断言
        getPhone() == "32434"
        getAddress() == "10 Bakers"
        getCity() == "London"
        getName() == "John Doe"
    }
}
👆可以利用分组断言来优化代码结构。此外,代码中的硬编码还有优化的余地:
def "Normal approval for a loan - improved"() {
    given: "a bank customer"
    Customer customer = new Customer(name:"John Doe",city:"London",address:"10 Bakers",phone:"32434")
    and: "his/her need to buy a house "
    int sampleTimeSpan=5
    int sampleAmount = 200.000
    Loan loan = new Loan(years:sampleTimeSpan, amount:sampleAmount)
    when:"a loan is requested"
    customer.requests(loan)
    then: "loan is approved as is"
    loanApprovedAsRequested(customer,loan,sampleTimeSpan,sampleAmount)
    and: "contact details are kept or record"
    contactDetailsMatchCustomer(customer,loan)
}
private void loanApprovedAsRequested(Customer customer,Loan loan,int originalYears,int originalAmount) {
    with(loan) {
        approved
        amount == originalAmount
        loan.years == originalYears
        loan.instalments == originalYears * 12
    }
    assert customer.activeLoans == 1
}
private void contactDetailsMatchCustomer(Customer customer,Loan loan ) {//封装
    with(loan.contactDetails) {
        phone == customer.phone
        address == customer.address
        city == customer.city
        name== customer.name
    }
}
👆封装的方法除了返回值为void外,还需遵循一定规则:
- 🍈使用
Spock的with方法 - 
then:块中重用交互验证
在
then:块中验证交互也需要一些技巧,先来看看原始代码: ```groovy def “Normal approval for a loan”() {given: “a bank customer” Customer customer = new Customer(name:”John Doe”,city:”London”,address:”10 Bakers”,phone:”32434”)
and: “his/her need to buy a house “ Loan loan = Mock(Loan)
when:”a loan is requested” customer.requests(loan)
then: “loan is approved as is” 1 loan.setApproved(true) 0 loan.setAmount() 0 * loan.setYears() * loan.getYears() >> 5 loan.getAmount() >> 200.000 _ loan.getContactDetails() >> new ContactDetails()
 
}
改造:
```groovy
def "Normal approval for a loan - alt"() {
    given: "a bank customer"
    Customer customer = new Customer(name:"John Doe",city:"London",address:"10 Bakers",phone:"32434")
    and: "his/her need to buy a house "
    Loan loan = Mock(Loan)
    when:"a loan is requested"
    customer.requests(loan)
    then: "loan request was indeed evaluated"
    interaction {
        loanDetailsWereExamined(loan)
    }
    and: "loan was approved as is"
    interaction {
        loanWasApprovedWithNoChanges(loan)
    }
}
private void loanWasApprovedWithNoChanges(Loan loan) {//封装断言
    1 * loan.setApproved(true)
    0 * loan.setAmount(_)
    0 * loan.setYears(_)
}
private void loanDetailsWereExamined(Loan loan) {
    _ * loan.getYears() >> 5
    _ * loan.getAmount() >> 200.000
    _ * loan.getContactDetails() >> new ContactDetails()
}
👆重点是这段代码:
interaction {
    loanDetailsWereExamined(loan)
}
Spock借此识别交互断言。
自定义
DSLGroovy支持自定义DSL,必要时可将interaction{}语法替换成自定义语法。感兴趣的读者可以参考**Groovy in Action, Second Edition, by Dierk Koenig et al. (Manning Publications, 2015)**
