4.4. Writing readable Spock tests

Despite all the cool facilities offered by Groovy, your ultimate target when writing Spock tests should be readability. Especially in large enterprise applications, the ease of refactoring is greatly affected by the quality of existing unit tests. Because unit tests also act as a live specification of the system, understanding Spock tests is crucial in cases requiring you to read a unit test to deduce the expected behavior of the code.

Knowing the basic techniques (for example, the Spock blocks) is only the first step to writing concise and understandable unit tests. The second step is to use the basic techniques effectively, avoiding the temptation of “sprinkling” unit test code with Groovy tricks that add no real purpose to the test other than showing off.[7]

7

If you really want to show off one-liners, Groovy is not for you. Learn Perl.

4.4.1. Structuring Spock tests

You saw all the Spock blocks at the beginning of the chapter. The given-when-then cycle should be your mantra when you start writing your first Spock unit tests. You might quickly discover that Spock doesn’t have many restrictions in regard to the number and sequence of blocks inside a test method. But just because you can mix and match Spock blocks doesn’t mean that you should.

As an example, it’s possible to have multiple when-then blocks in a single test method, as shown in the following listing.

Listing 4.24. Multiple when-then blocks

This pattern must be used with care. It can be used correctly as a way to test a sequence of events (as demonstrated in this listing). If used incorrectly, it might also mean that your test is testing two things and should be broken.

Use common sense when you structure Spock tests. If writing descriptions next to Spock blocks is becoming harder and harder, it might mean that your test is doing complex things.

4.4.2. Ensuring that Spock tests are self-documenting

I’ve already shown you the @Subject and @Title annotations and explained that the only reason they’re not included in all examples is to save space.

What I’ve always included, however, are the descriptions that follow each Spock block. Even though in Spock these are optional, and a unit test will run without them, you should consider them essential and always include them in your unit tests. Take a look at the following listing for a real-world antipattern of this technique.

Listing 4.25. Missing block descriptions—don’t do this

This test lacks any kind of human-readable text. It’s impossible to understand what this test does without reading the code. It’s also impossible to read it if you’re a nontechnical person. In this example, it’s a pity that the name of the test method isn’t a plain English sentence (a feature offered natively by Spock). You could improve this test by changing the block labels as follows:

setup: "given a naming pattern for product files"
expect: "that the file name matches regardless of spaces and capitalization"
where: "some possible file names are"

Always include in your Spock tests at least the block descriptions and make sure that the test method name is human-readable.

4.4.3. Modifying failure output

Readability shouldn’t be constrained to successful unit tests. Even more important is the readability of failed tests. In a large application with legacy code and a suite of existing unit tests, a single change can break unit tests that you didn’t even know existed.

You learned how Groovy asserts work in chapter 2 and how Spock gives you much more information when a test fails. Although Spock automatically analyzes simple types and collections, you have to provide more hints when you assert your own classes. As an example, the following listing adds one of the products in the basket twice.

Listing 4.26. Adding a product twice in the basket

I’ve introduced a bug in the Basket class. When the test fails, you get the output shown in figure 4.14.

Figure 4.14. Failed Spock test with custom class

Because Basket.java is a class unknown to Spock, it can’t give you detailed information about what went wrong. According to this result, the total weight of the basket is now 7 kilograms, even though all products weigh 9 kg. To debug this unit test, you’d have to run it in a debugger and find the source of the mistake in the Basket class.

To help Spock do its magic, you can override the toString() method in your objects, because this is what Spock runs on failed tests. The following listing exposes the internal implementation of the basket in the toString() method.

Listing 4.27. Helping failure rendering in the toString() method

Now when the test fails, you get the output shown in figure 4.15.

Figure 4.15. Spock failed test with custom toString() method

Seeing this result makes it much easier to understand what’s gone wrong. Just by looking at the test result, you can see that even though you added two cameras in the basket, it kept only one. The bug you inserted is exactly at this place (it always adds one product in the basket, regardless of what the user said).

This kind of detail is a lifesaver when multiple tests break and it’s hard to understand whether the test needs fixing or the production code you changed is against specifications. In a large enterprise application, a single code change can easily break hundreds of existing unit tests. It’s critical to understand which tests broke because your change is wrong, and which tests broke because they’re based on old business needs that are made obsolete by your change. In the former case, you must revise your code change (so that tests pass), whereas in the latter case, you need to update the failing unit tests themselves so that they express the new requirement.

The beauty of this Spock feature is that toString() is usually already implemented in domain objects for easy logging and reporting. You may be lucky enough to get this functionality for free without any changes in your Java code.

After you finish writing a Spock test, check whether you need to implement custom toString() methods for the classes that are used in the final assertions.

4.4.4. Using Hamcrest matchers

Hamcrest matchers[8] are a third-party library commonly used in JUnit assert statements. They offer a pseudo-language that allows for expressiveness in what’s being evaluated. You might have seen them already in JUnit tests.

8

Hamcrest is an anagram of the word matchers.

Spock supports Hamcrest matchers natively, as shown in the following listing.

Listing 4.28. Spock support for Hamcrest matchers

The hasItem() matcher accepts a list and returns true if any element matches the argument. Normally, that check would require a loop in Java, so this matcher is more brief and concise.

One of the important features of Hamcrest matchers is that they can be chained together to create more-complicated expressions. Listing 4.27 also uses the not() matcher, which takes an existing matcher and reverses its meaning. Figure 4.16 illustrates this test. You can find more information about other Hamcrest matchers (and how to create your own) on the official web page at http://hamcrest.org/.

Figure 4.16. Hamcrest matchers can be used natively within Spock tests.

Spock also supports an alternative syntax for Hamcrest matchers that makes the flow of reading a specification more natural, as shown in the next listing.

Listing 4.29. Alternative Spock support for Hamcrest matchers

The test is exactly the same as listing 4.28, but reads better because the matcher lines are coupled with the Spock blocks. The assertions are close to human text: “expect products has item (named) camera, and that products (does) not have item (named) hotdog” (see figure 4.17).

Figure 4.17. Hamcrest matchers have an alternate near-English syntax that makes them easier to read.

The expect() and that() methods are Spock syntactic sugar and have no effect on how the test runs.

Compatibility with JUnit

As you’ve seen, Spock allows you to reuse several existing JUnit facilities. I’ve already mentioned that JUnit lifecycle annotations (@Before, @After, and so on) are recognized by Spock. Now you’ve seen that integration with Hamcrest matchers is also supported. Spock even supports JUnit rules out of the box. The transition to Spock is easy because it doesn’t force you to discard your existing knowledge. If your team has invested heavily in custom matchers or rules, you can use them in your Spock tests, too.

Hamcrest matchers have their uses, and they can be powerful if you create your own for your domain classes. But often they can be replaced with Groovy code, and more specifically with Groovy closures. The following listing shows the same trivial example without Hamcrest matchers.

Listing 4.30. Using Groovy closures in Spock assertions

I consider Groovy closures more powerful because they can be created on the spot for each unit test to match exactly what’s being tested. But if you have existing Hamcrest matchers from your JUnit tests, using them in Spock tests is easy, as shown in listings 4.28 and 4.29.

As a general rule, if a Hamcrest matcher already covers what you want, use it (hasItem() in the preceding example). If using Hamcrest matchers makes your example complex to read, use closures.

4.4.5. Grouping test code further

I mentioned at the beginning of the chapter that one of the first problems you encounter in large enterprise projects is the length of unit tests. With Spock blocks, you already have a basic structure in place because the setup-trigger-evaluate cycles are clearly marked. Even then, you’ll find several times that your then: and given: blocks contain too many things, making the test difficult to read.

To better illustrate this problem, you’ll add to the running example a class that represents the warehouse of the e-shop, as shown in the following listing.

Listing 4.31. An imaginary warehouse

You’ll also augment the electronic basket with more (imaginary) methods that define its behavior, as shown in the following listing.

Listing 4.32. An enterprisy basket

Now assume that you want to write a unit test for the warehouse to verify that it works correctly when a customer checks out. The Spock test is shown in the next listing.

Listing 4.33. Assertions and setup on the same object

You’ve already split the given: and when: blocks with and: blocks in order to make the test more readable. But it can be improved even more in two areas:

  • The final assertions test multiple things, but all on the same object.
  • The given: block of the test has too many statements, which can be roughly split into two kinds: statements that create objects, and statements that set properties on existing objects.

In most cases (involving large Spock tests), extra properties are secondary to the object creation. You can make several changes to the test, as shown in the following listing.

Listing 4.34. Grouping similar code with Groovy and Spock

First, you can group all assertions by using the Spock with() construct. This feature is specific to Spock and allows you to show that multiple assertions affect a single object. It’s much clearer now that you deal specifically with the warehouse inventory at the end of this test.

The Spock with() construct is inspired from the Groovy with() construct that works on any Groovy code (even outside Spock tests). I’ve used this feature in the given: and when: blocks to group all setup code that affects a single object. Now it’s clearer which code is creating new objects and which code is setting parameters on existing objects (indentation also helps).

Notice that the two with() constructs may share the same name but are unrelated. One is a Groovy feature, and the other is a Spock feature that works only in Spock asserts.

As an added bonus, I’ve also used the Groovy convention demonstrated in chapter 2, where you can remove parentheses in method calls with at least one argument. This makes the test a little more like DSL. It’s not much, but it certainly helps with readability. I’ll show more ways to deal with large Spock tests in chapter 8.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.200.109