本书中的例子并非贴合生产,大多例子都非常简单。企业应用的单元测试通常具有大量代码。即使是“纯粹”的单元测试,也有必不可少的准备工作。通过使用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)**
