本书中的例子并非贴合生产,大多例子都非常简单。企业应用的单元测试通常具有大量代码。即使是“纯粹”的单元测试,也有必不可少的准备工作。通过使用Groovy/Spockwith()方法可以解决一部分问题(第四章)。此节将更进一步,解决其他问题。
image.png
此例是银行贷款应用。且有5个主要类:

  • 🍏Custom.java
  • 🍐Loan.java
  • 🍑CreditCard.java
  • 🍒ContactDetails.java
  • 🍓BankAccount.java

    使用封装方法提高可读性

    第四章讲述了保持when:块精简的重要性,但是在大型应用里,长代码段不可避免:

    1. def "a bank customer with 3 credit cards is never given a loan"() {
    2. given: "a customer that wants to get a loan"
    3. Customer customer = new Customer(name:"John Doe")
    4. and: "his credit cards"//冗长的代码
    5. BankAccount account1 = new BankAccount()
    6. account1.with {
    7. setNumber("234234")
    8. setHolder("John doe")
    9. balance=30
    10. }
    11. CreditCard card1 = new CreditCard("447978956666")
    12. card1.with{
    13. setHolder("John Doe")
    14. assign(account1)
    15. }
    16. customer.owns(card1)
    17. BankAccount account2 = new BankAccount()
    18. account2.with{
    19. setNumber("3435676")
    20. setHolder("John Doe")
    21. balance=30
    22. }
    23. CreditCard card2 = new CreditCard("4443543354")
    24. card2.with{
    25. setHolder("John Doe")
    26. assign(account2)
    27. }
    28. customer.owns(card2)
    29. BankAccount account3 = new BankAccount()
    30. account2.with{
    31. setNumber("45465")
    32. setHolder("John Doe")
    33. balance=30
    34. }
    35. CreditCard card3 = new CreditCard("444455556666")
    36. card3.with{
    37. setHolder("John Doe")
    38. assign(account3)
    39. }
    40. customer.owns(card3)
    41. when:"a loan is requested"//简洁易读
    42. Loan loan = new Loan()
    43. customer.requests(loan)
    44. then: "loan should not be approved"//简洁易读
    45. !loan.approved
    46. }

    👆虽然所有块都添加了描述,断言也很清晰。但是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外,还需遵循一定规则:

  • 🍈使用Spockwith方法
  • 🍉或者使用Groovy的断言assert关键字

    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借此识别交互断言。

自定义DSL Groovy支持自定义DSL,必要时可将interaction{}语法替换成自定义语法。感兴趣的读者可以参考**Groovy in Action, Second Edition, by Dierk Koenig et al. (Manning Publications, 2015)**