本书中的例子并非贴合生产,大多例子都非常简单。企业应用的单元测试通常具有大量代码。即使是“纯粹”的单元测试,也有必不可少的准备工作。通过使用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
借此识别交互断言。
自定义
DSL
Groovy
支持自定义DSL
,必要时可将interaction{}
语法替换成自定义语法。感兴趣的读者可以参考**Groovy in Action, Second Edition, by Dierk Koenig et al. (Manning Publications, 2015)**