The projects in most examples so far are trivial projects designed as a learning material 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.
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:
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.
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 example, let’s see a unit test that has a long setup process, shown in the next listing.
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 variables, 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.
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.
Here you extract the common code into a helper method. The helper method has the following positive effects:
The added advantage of helper methods is that you can share them across test methods 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.
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 project, you might modify the Spock test as shown in the following listing.
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.
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.
Again, as a starting example of a questionable design, let’s start with a big then: block, as shown in the next listing.
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]
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.
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.
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.
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:
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.
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]
In this example, mocking the loan class is overkill. I mock it for illustration purposes only to show you helper methods with mocks.
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 interaction check, as seen in the next listing.
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 format 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.
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.
18.221.249.198