When you create a unit test to examine the behavior of a specific class, you’ll find yourself writing the same code over and over again in the given: block. This makes sense because several test methods have the same initial state and only a different trig- ger (the when: block). Instead of copying and pasting this behavior (and thus violating the DRY6 principle), Spock offers you several facilities to extract common precondi- tions and post-conditions of tests in their own methods.
4.3.1 Setup and cleanup of a feature
In the Spock test that examines your imaginary electronic basket, I’ve duplicated the code that creates products multiple times. This code can be extracted as shown in the following listing.
class CommonSetupSpec extends spock.lang.Specification{
Product tv
Product camera def setup() {
tv = new Product(name:"bravia",price:1200,weight:18) camera = new Product(name:"panasonic",price:350,weight:2) }
def "A basket with one product weights as that product"() { [...code redacted for brevity purposes...]
}
def "A basket with two products weights as their sum"() [...code redacted for brevity purposes...]
} }
6 This acronym stands for don’t repeat yourself. See https://en.wikipedia.org/wiki/Don’t_repeat_yourself for more information.
Listing 4.19 Extracting common initialization code
Common classes
are placed as fields. This method runs automatically before each test method.
Initialization code is written once.
Test methods run after initialization code.
Spock will detect a special method called setup() and will run it automatically before each test method. In a similar manner, Spock offers a cleanup() method that will run after each test method finishes. A full example is shown in the following listing.
@Subject(Basket)
class CommonCleanupSpec extends spock.lang.Specification{
Product tv Product camera Basket basket def setup() { tv = new Product(name:"bravia",price:1200,weight:18) camera = new Product(name:"panasonic",price:350,weight:2) basket = new Basket()
}
def "A basket with one product weights as that product"() { when: "user wants to buy the TV"
basket.addProduct tv
then: "basket weight is equal to all product weight"
basket.currentWeight == tv.weight }
def "A basket with two products weights as their sum"() { when: "user wants to buy the TV and the camera"
basket.addProduct tv basket.addProduct camera
then: "basket weight is equal to all product weight"
basket.currentWeight == (tv.weight + camera.weight) }
def cleanup() {
basket.clearAllProducts() }
}
As with the cleanup: block, the cleanup() method will always run, regardless of the result of the test. The cleanup() method will even run if an exception is thrown in a test method.
4.3.2 Setup and cleanup of a specification
The code you place inside the setup() and cleanup() methods will run once for each test method. If, for example, your Spock test contains seven test methods, the setup/
cleanup code will run seven times as well. This is a good thing because it makes each test method independent. You can run only a subset of test methods, knowing they’ll be correctly initialized and cleaned afterward.
Listing 4.20 Extracting common pre/post conditions
Common classes are placed as fields.
This method will run automatically before each test method.
This method will run automatically after each test method.
111 Exploring the lifecycle of a Spock test
But sometimes you want initialization code to run only once before all test methods.
This is the usual case when you have expensive objects that will slow down the test if they run multiple times. A typical case is a database connection that you use for integration tests, but any long-lived expensive object is a good candidate for running only once.
Spock supports this case as well, as shown in the following listing.
class LifecycleSpec extends spock.lang.Specification{
def setupSpec() { println "Will run only once"
}
def setup() { println "Will run before EACH feature"
}
def "first feature being tested"() { expect: "trivial test"
println "first feature runs"
2 == 1 +1 }
def "second feature being tested"() { expect: "trivial test"
println "second feature runs"
5 == 3 +2 }
def cleanup() { println "Will run once after EACH feature"
}
def cleanupSpec() { println "Will run once at the end"
} }
If you run this unit test, it will print the following:
Will run only once
Will run before EACH feature first feature runs
Will run once after EACH feature Will run before EACH feature second feature runs
Will run once after EACH feature Will run once at the end
Listing 4.21 All Spock lifecycle methods
Compatibility with JUnit lifecycle methods
If you’re familiar with JUnit, you’ll notice that the Spock lifecycle methods work exactly like the annotations @Before, @After, @BeforeClass, and @AfterClass. Spock hon- ors these annotations as well, if for some reason you want to continue to use them.
Initialization for expensive objects Common code for all tests
Test methods
Common cleanup code for all tests
Finalization of expensive objects
Because setupSpec() and cleanupSpec() are destined to hold only long-lived objects that span all the test methods, Spock allows code in these methods to access only static fields (not recommended) and objects marked as @Shared, as you’ll see in the next section.
4.3.3 Long-lived objects with the @Shared annotation
You can indicate to Spock which objects you want to survive across all test methods by using the @Shared annotation. As an example, assume that you augment your elec- tronic basket with a credit card processor:
public class CreditCardProcessor { public void newDayStarted() {
[...code redacted for brevity..]
}
public void charge(int price) {
[...code redacted for brevity..]
}
public int getCurrentRevenue() {
[...code redacted for brevity..]
}
public void shutDown() {
[...code redacted for brevity..]
} }
CreditCardProcessor is an expensive object. It connects to a bank back end and allows your basket to charge credit cards. Even though the bank has provided dummy credit card numbers for testing purposes, the initialization of the connection is slow. It would be unrealistic to have each test method connect to the bank again. The follow- ing listing shows the solution to this problem.
class SharedSpec extends spock.lang.Specification{
@Shared
CreditCardProcessor creditCardProcessor;
BillableBasket basket def setupSpec() {
creditCardProcessor = new CreditCardProcessor() }
Listing 4.22 Using the @Shared annotation
Will be created only once Will be created multiple times
Expensive/slow initialization
113 Exploring the lifecycle of a Spock test
def setup() {
basket = new BillableBasket() creditCardProcessor.newDayStarted() basket.setCreditCardProcessor(creditCardProcessor) }
def "user buys a single product"() { given: "an empty basket and a TV"
Product tv = new Product(name:"bravia",price:1200,weight:18)
and: "user wants to buy the TV"
basket.addProduct(tv) when: "user checks out"
basket.checkout()
then: "revenue is the same as the price of TV"
creditCardProcessor.currentRevenue == tv.price }
def "user buys two products"() {
given: "an empty basket and a camera"
Product camera = new
Product(name:"panasonic",price:350,weight:2) and: "user wants to two cameras"
basket.addProduct(camera,2) when: "user checks out"
basket.checkout()
then: "revenue is the same as the price of both products"
creditCardProcessor.currentRevenue == 2 * camera.price }
def cleanup() {
basket.clearAllProducts() }
def cleanupSpec() {
creditCardProcessor.shutDown() }
}
Here you mark the expensive credit card processor with the @Shared annotation. This ensures that Spock creates it only once. On the other hand, the electronic basket itself is lightweight, and therefore it’s created multiple times (once for each test method).
Notice also that the credit card processor is closed down once at the end of the test.
4.3.4 Use of the old() method
The old() method of Spock is a cool trick, but I’ve yet to find a real example that makes it worthwhile. I mention it here for completeness, and because if you don’t
Fast/cheap initialization Shared object
can be used normally.
Will run multiple times
Will run only once
know how it works, you might think it breaks the Spock lifecycle principles. You use it when you want your test to capture the difference from the previous state instead of the absolute value, as shown in the following listing.
def "Adding a second product to the basket increases its weight"() { given: "an empty basket"
Basket basket = new Basket()
and: "a tv is already added to the basket"
Product tv = new Product(name:"bravia",price:1200,weight:18) basket.addProduct(tv) when: "user gets a camera too"
Product camera = new Product(name:"panasonic",price:350,weight:2) basket.addProduct(camera) then: "basket weight is updated accordingly"
basket.currentWeight == old(basket.currentWeight) + camera.weight }
Here you have a unit test that checks the weight of the basket after a second product is added. You could check for absolute values in the then: block (assert that the basket weight is the sum of two products), but instead you use the old() method and say to Spock, “I expect the weight to be the same as before the when: block, plus the weight of the camera.” Figure 4.13 illustrates this.
The difference in expression is subtle, and if you find the old() method confus- ing, there’s no need to use it at all.
Listing 4.23 Asserting with the old() method
Product is added in given: block.
Second product is added in when: block.
Checking the difference in weight
given:
20 kg
18 kg 2 kg
==
and: when: then:
20 kg 18 kg 2 kg
==
then:
then: block uses old( ) to evaluate weights using the basket weight prior to the when: block.
old(basket.currentWeight)
then: block evaluates absolute values of product weights in the basket.
Figure 4.13 The old() method allows you to access values set before the when: block in a test.
115 Writing readable Spock tests