Handling large Spock tests

Một phần của tài liệu Manning java testing with spock (Trang 260 - 270)

The projects in most examples so far are trivial projects designed as a learning mate- rial instead of production-quality applications. In the real world, enterprise projects come with huge code bases that directly affect the size of unit tests.

Even in the case of pure unit tests (non-integration tests), preparing the class under test and its collaborators is often a lengthy process with many statements and boilerplate code that’s essential for the correct functionality of the Java code tested, but otherwise unrelated to the business feature being tested.

I’ve provided some hints for making clear the intention of Spock tests using Groovy with() and Spock with() methods, as seen in chapter 4. In this section, you’ll take this grouping of statements one step further by completely refactoring the respective statements in their own methods.

3 You can also ignore exceptions during cleanup if you use the annotation like @AutoCleanup(quiet = true), but I don’t endorse this practice unless you know what you’re doing.

4 Yes, I know that expecting exceptions does not happen via annotations. Thanks for catching it!

The running example here is a loan-approval application, shown in figure 8.4.

The Java classes that take part in the system are as follows:

■ Customer.java

■ Loan.java

■ CreditCard.java

■ ContactDetails.java

■ BankAccount.java

You can find the full source code in the GitHub repository of the book,5 but notice that most classes are only skeletons designed to demonstrate specific techniques in the Spock tests.

8.2.1 Using helper methods to improve code readability

Chapter 4 stressed the importance of the when: block and how critical it is to keep its code short and understandable. But in big enterprise projects, long code segments can appear in any Spock block, harming the readability of the test. As a starting exam- ple, let’s see a unit test that has a long setup process, shown in the next listing.

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")

5 https://github.com/kkapelon/java-testing-with-spock/tree/master/chapter8/src/main/java/com/

manning/spock/chapter8/loan

Listing 8.15 A Spock test with long setup—don’t do this

Customer

Loan request

Bank

Approved

Rejected

$

Figure 8.4 A customer requests a loan from a bank. The bank approves or rejects the loan.

A badly designed and: block. It contains too much code.

237 Handling large Spock tests

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 }

At first glance, this unit test correctly follows the best practices outlined in chapter 4.

All the blocks have human-readable descriptions, the when: block clearly shows what’s being tested (a loan request), and the final result is also clear (either the loan is approved or it’s rejected).

The setup of the test, however, is a gigantic piece of code that’s neither clear nor directly relevant to the business case tested. The description of the block talks about credit cards but contains code that creates both credit cards and bank accounts (because apparently a credit card requires a valid bank account in place).

Even with the use of the with() method for grouping several statements that act on the same project, the setup code makes the test hard to read. It contains a lot of vari- ables, and it’s not immediately clear whether they affect the test. For example, does it matter that the account balance is $30 in each connected account? Does this affect the approval of the loan? You can’t answer that question by reading the Spock test.

A well-designed when:

block. Code is short.

A well-designed then:

block. Code is short.

In such cases, a refactoring must take place so that the intention of the test becomes clear and concise. Large amounts of code should be extracted to helper methods, as shown in the next listing.

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 }

Here you extract the common code into a helper method. The helper method has the following positive effects:

■ It reduces the amount of setup code.

■ It clearly shows that the setup code is a set of sample credit cards.

■ It hides the fact that a bank account is needed for creating a credit card (as this is unrelated to the approval of a loan).

■ It shows by its arguments that the holder of the credit card must be the same as the customer who requests the loan.

The added advantage of helper methods is that you can share them across test meth- ods or even across specifications (by creating an inheritance among Spock tests, for example). You should therefore design them so they can be reused by multiple tests.

Listing 8.16 Spock test with helper methods

Setup code is now short and clear

A helper method that deals with

credit card The fact that each credit card

needs a bank account is hidden in the helper method.

This helper method creates a credit card.

239 Handling large Spock tests

Depending on your business case, you can further refine the helper methods you use to guide the reader of the test to what exactly is being tested. In a real-world proj- ect, you might modify the Spock test as shown in the following listing.

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 }

This improved listing makes minor adjustments to the arguments of the helper method. First, you use a single variable for the customer name. This guards against any spelling mistakes so you can be sure that all credit cards are assigned to the same customer (because as the description of the test says, the number of credit cards of the customer is indeed examined for loan approval).

Second, you replace the credit card numbers with dummy strings. This helps the reader of the test understand that the number of each credit card isn’t used in loan approval.

As a final test, you add an expect: block (as demonstrated in chapter 4) that strengthens the readability of the setup code.

After all these changes, you can compare listings 8.15 with 8.17. In the first case, you have a huge amount of setup code that’s hard to read, whereas in the second case, you can understand in seconds that the whole point of the setup code is to assign credit cards to the customer.

8.2.2 Reusing assertions in the then: block

Helper methods should be used in all Spock blocks when you feel that the size of the code gets out of hand. But because of technical limitations, the creation of helper methods for the then: block requires special handling.

Listing 8.17 Using arguments that imply their importance in the test

Enforces the same customer for the loan and credit cards Makes it clear that

credit card numbers are unused in loan approval

Explicitly verifies the result of the setup

Again, as a starting example of a questionable design, let’s start with a big then:

block, as shown in the next listing.

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 }

Here the then: block contains multiple statements with different significance. First, you have some important checks that confirm that the loan is indeed approved. Then you have other checks that examine the details of the approved loan (and especially the fact that they match the customer who requests it). Finally, it’s not clear whether the numbers and strings that take part in the then: block are arbitrary or depend on something else.6

As a first step to improve this test, you’ll split the then: block into two parts and group similar statements, as shown in the following listing.

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

Listing 8.18 Spock test with dubious then: block

6 In this simple example, it’s obvious that the contact details of the loan are the same as the customer ones. In a real-world unit test, this isn’t usually the case.

Listing 8.19 Improved Spock test with clear separation of checks

These examine the loan approval.

These checks are secondary.

Makes clear the connection between expected results

241 Handling large Spock tests

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"

} }

The improved version of the test clearly splits the checks according to the business case. You’ve replaced the number 60, which was previously a magic number, with the full logic that installments are years times 12 (for monthly installments).

The code that checks loan details still has hardcoded values. You can further improve the code by using helper methods, as shown in the next listing.

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) }

Listing 8.20 Using helper methods for assertions

Grouping of primary loan checks

Makes clear the expected result

Different block for secondary checks

Helper methods with descriptive names

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 }

}

This listing refactors the two separate blocks into their own helper methods. The important thing to note is the format of each helper method.

Your first impulse might be to design each helper method to return a Boolean if all its assertions pass, and have Spock check the result of that single Boolean. This doesn’t work as expected.

The recommended approach, as shown in listing 8.20, is to have helper methods as void methods. Inside each helper method, you can put one of the following:

■ A group of assertions with the Spock with() method

■ A Groovy assert but with the assert keyword prepended Notice this line:

assert customer.activeLoans == 1

Because this statement exists in a helper method and not directly in a then: block, it needs the assert keyword so Spock can understand that it’s an assertion. If you miss the assert keyword, the statement will pass the test regardless of the result (which is a bad thing).

This listing also refactors the second helper method to validate loan details against its arguments instead of hardcoded values. This makes the helper method reusable in other test methods where the customer could have other values.

Spend some time comparing listing 8.20 with the starting example of listing 8.18 to see the gradual improvement in the clarity of the unit test.

with() method works as expected in helper method.

assert keyword is needed in helper method.

Clear connection between loan and customer who requested it

243 Handling large Spock tests

8.2.3 Reusing interactions in the then: block

As you saw in the previous section, Spock needs some help to understand assertions in helper methods. A similar case happens with mocks and interactions.

The following listing shows an alternative Spock test, in which the loan class is mocked instead of using the real class.7

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() }

The test in this listing contains multiple interaction checks in the then: block that have a different business purpose. The Loan class is used in this case both as a mock and as a stub. This fact is implied by the cardinalities in the interaction checks.

You can improve this test by making clear the business need behind each interac- tion check, as seen in the next listing.

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)

Listing 8.21 Spock tests with questionable then: block

7 In this example, mocking the loan class is overkill. I mock it for illustration purposes only to show you helper methods with mocks.

Listing 8.22 Explicity declaring helper methods with interactions Primary checks

for the loan

Stubbed methods needed for the correct functioning of the test

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() }

You’ve created two helper methods and added a then: block. The first helper method holds the primary checks (the approval of the loan with its original values). The other helper method is secondary, as it contains the stubbed methods of the loan object (which are essential for the test but not as important as the approval/rejection status of the loan).

The important thing to understand in this listing is that you wrap each helper method in an interaction block:

interaction {

loanDetailsWereExamined(loan) }

This is needed so that Spock understands the special format of the N*class.method (N) interaction check, as shown in chapter 6. Spock automatically understands this for- mat in statements found directly under the then: block, but for helper methods you need to explicitly tell Spock that statements inside the method are interaction checks.

Constructing a custom DSL for your testing needs

The Groovy language is perfect for creating your own domain-specific language (DSL) that matches your business requirements. Rather than using simple helper methods, you can take your Spock tests to the next level by creating a DSL that matches your business vocabulary. Creating a DSL with Groovy is outside the scope of this book, so feel free to consult chapter 19 of Groovy in Action, Second Edition, by Dierk Koenig et al. (Manning Publications, 2015) for more information on this topic.

Interaction blocks are needed for helper methods that contain mocks.

Helper methods named after the business check

245 Creating partial mocks with spies

Một phần của tài liệu Manning java testing with spock (Trang 260 - 270)

Tải bản đầy đủ (PDF)

(306 trang)