I released five new sample lessons from my Test With Spring course: Introduction to Spock Framework

Writing Unit Tests With Spock Framework: Introduction to Specifications, Part Two

The previous part of this tutorial described the structure of a Spock specification and helped us to write our first specification.

Although it is important to understand the basics, our specifications are not very useful because we don’t know how we can describe the expected behavior of the system under specification.

This blog post fixes that problem. Let’s start by taking a look at the structure of a feature method.

Additional Reading:

If you are not familiar with Spock Framework, you should read the following blog posts before you continue reading this blog post:

The Structure of a Feature Method

A feature method describes the expected behavior of a feature that is implemented by the system under specification. We can name our feature methods by using String literals, and this is a huge advantage over JUnit (or TestNG) because we can use sentences that actually make sense.

The source code of a simple specification that has one feature method looks as follows:

import spock.lang.Specification

class MapSpec extends Specification {

    def 'Get value from a map'() {
    }
}

Every feature method consists of so-called blocks. Each block has a label, and the body of the block extends to the beginning of the next block or to the end of the feature method. A feature method might have the following blocks:

  • The setup block must be the first block of a feature method and it contains the configuration of the described feature. A feature method can have only 1 setup block.
  • The when and then block describes the stimulus (when) and the expected response (then). A feature method can have multiple when and then blocks.
  • The expect block describes the stimulus and the expected response in a single expression. A feature method can have only one expect block, and it is possible to add both when and then and expect blocks into the same feature methods. However, this is not be very practical.
  • The cleanup block is used to cleanup the resources used by a feature method, and it is invoked even if the feature method throws an exception. A feature method can have only one cleanup block.
  • The where block must be the last block of a feature method, and it is used to write data-driven feature methods. A feature method can have only one where block.

The following figure illustrates the structure of a feature method:

featuremethodstructure

In other words, the structure of a feature method looks as follows:

import spock.lang.Specification

class MapSpec extends Specification {

    def 'Get value from a map'() {
		//setup block
		//when and then blocks
		//expect block
		//cleanup block
		//where block
    }
}

Let’s move on and write our first feature methods.

Writing Feature Methods

We can write feature methods by using the blocks mentioned in the previous section. Let’s start by finding out how we can use the setup block.

Using the Setup Block

As we already know, the setup block contains the setup code of the described feature. We can create a setup block by using the label: setup and adding the setup code after that label. For example, if we want to create a HashMap object and put one value into the created map, our setup block looks as follows:

import spock.lang.Specification

class MapSpec extends Specification {

    def 'Get value from a map'() {

        setup:
        def key = 'key'
        def value = 1

        def map = new HashMap()
        map.put(key, value)
    }
}

However, the setup label is optional. If we decide to omit it, we create an implicit setup block that looks as follows:

import spock.lang.Specification

class MapSpec extends Specification {

    def 'Get value from a map'() {

        def key = 'key'
        def value = 1

        def map = new HashMap()
        map.put(key, value)
    }
}

If we want to write our feature methods by using the given-when-then format, we can replace the setup label with the label: given and describe our setup block by using a String literal. A setup block that uses this approach looks as follows:

import spock.lang.Specification

class MapSpec extends Specification {

    def 'Get value from a map'() {

        given: 'Map contains one key-value pair'
        def key = 'key'
        def value = 1

        def map = new HashMap()
        map.put(key, value)
    }
}

After we have configured the described feature, we have to specify its behavior. Let’s find out how we can do it by using when and then blocks.

Using When and Then Blocks

We can specify the behavior of the described feature by using when and then blocks which always occur together. The when block describes the stimulus and the then block describes the expected response.

We can create a when block by following these rules:

  • A when block must start with the label: when.
  • A when block can have an additional description that is given by using a String literal.
  • A when block can contain any code.

We can create a then block by following these rules:

  • A then block must be placed right after the when block that describes the stimulus.
  • A then block must start with the label: then.
  • A then block can have an additional description that is given by using a String literal.
  • A then block can contain only variable definitions, conditions, exception conditions and interactions.
Interactions describe how the system under specification should interact with mock objects. We will talk more about this in future.

If we want to verify that our map returns the correct value when a value is found with the given key, we have to add the following when and then block into our feature method:

import spock.lang.Specification

class MapSpec extends Specification {

    def 'Get value from a map'() {

        given: 'Map contains one key-value pair'
        def key = 'key'
        def value = 1

        def map = new HashMap()
        map.put(key, value)

        when: 'A value is found with the given key'
        def found = map.get(key)

        then: 'Should return the found value'
        found == value
    }
}

We can also add multiple when and then blocks into a feature method. For example, if we want to verify that our map returns the correct value in every situation (a value is found and a value is not found), we have to add the following when and then blocks into our feature method:

import spock.lang.Specification

class MapSpec extends Specification {

    def 'Get value from a map'() {

        given: 'Map contains one key-value pair'
        def incorrectKey = 'incorrectKey'
        def key = 'key'
        def value = 1

        def map = new HashMap()
        map.put(key, value)

        when: 'A value is found with the given key'
        def found = map.get(key)

        then: 'Should return the found value'
        found == value

        when: 'A value is not found with the given key'
        found = map.get(incorrectKey)

        then: 'Should return null'
        found == null
    }
}

When we take a closer look at the feature methods described in this section, we notice that the when block seems a bit artificial. Let’s move on and find out how we can write the same feature method by using the expect block.

Using the Expect Block

An expect block describes the stimulus and expected response in a single expression. We can create an expect block by following these rules:

  • An expect block must start with the label: expect.
  • An expect block can have an additional description that is given by using a String literal.
  • An expect block can contain only conditions and variable definitions.

Let’s rewrite our feature method which verifies that our map returns the correct value when a value is found with the given key. We can do this by replacing the when and then block with the following expect block:

import spock.lang.Specification

class ExpectSpec extends Specification {

    def 'Get value from a map'() {

        given: 'Map contains one key-value pair'
        def key = 'key'
        def value = 1

        def map = new HashMap()
        map.put(key, value)

        expect: 'Should return the found value when a value is found with the given key'
        map.get(key) == value
    }
}

However, our job is not done yet. We still have to verify that our map returns the correct value when a value is not found with the given key. We can do this in a clean way by using the and label. The and label is used to describe individual parts of a block, and it has an optional description that is given by using a String literal.

We can finish our feature method by adding the following lines into it:

import spock.lang.Specification

class ExpectSpec extends Specification {

    def 'Get value from a map'() {

        given: 'Map contains one key-value pair'
        def incorrectKey = 'incorrectKey'
        def key = 'key'
        def value = 1

        def map = new HashMap()
        map.put(key, value)

        expect: 'Should return the found value when a value is found with the given key'
        map.get(key) == value

        and: 'Should return null when a value is not found with the given key'
        map.get(incorrectKey) == null
    }
}
The Spock documentation provides a guideline that helps us to select to correct approach for describing the behavior of a feature:

As a guideline, use when-then to describe methods with side effects, and expect to describe purely functional methods.

We are now aware how we can describe the tested feature by using both when and then and expect blocks. That is a good start, but sometimes our feature method reserves resources that must be freed afterwards. Let’s find out how we can clean up these resources by using the cleanup block.

Using the Cleanup Block

A cleanup block is used to free any resources used by a feature method, and Spock guarantees that it is invoked even if the feature method throws an exception. We can create a cleanup block by following these rules:

  • A cleanup block must start with the label: cleanup.
  • A cleanup block must be placed after the when and then and/or expect blocks.

Let’s assume that we have a feature method which creates a new file. Naturally we want to delete the created file after the feature method is finished. We can do this by adding the following cleanup block into our feature method:

import spock.lang.Specification

class FileSpec extends Specification {

	def 'Create a new file'() {

		setup:
		def file = new File("/tmp/foo.txt")
		
		when: 'A new file is created'
		file.createNewFile()
		
		then: 'Should create a new file'
		file.exists() == true 
		file.isFile() == true
		file.isDirectory() == false

		cleanup:
		file.delete()
    }
}

We have now taken a quick look at the structure of a feature method and written our first feature methods. Let’s summarize what we learned from this blog post.

Summary

This blog post has taught us four things:

  • A feature method consists of blocks.
  • Each block has a label, and the body of the block extends to the beginning of the next block or to the end of the feature method.
  • A feature method might have the following blocks: setup, when and then, expect, cleanup, and where.
  • We can describe individual parts of a block by using the and label.

The next part of this tutorial provides an introduction to data-driven testing.

P.S. You can get the example application of this blog post from Github.

About the Author

Petri Kainulainen is passionate about software development and continuous improvement. He is specialized in software development with the Spring Framework and is the author of Spring Data book.

About Petri Kainulainen →

3 comments… add one
  • Excellent Post Petri, it was clear and concise and was exactly what i was looking for. How would one go about using Spock to test Rest Clients , their expected JSON return and write tests so that multiple conditions must be true before test can be a pass.

    Reply
    • Hi Ron,

      Do you want to want to isolate the REST client from the actual REST API or do you want to write an end-to-end test that tests both the client and the REST API? The reason why I ask this is that I am not 100% sure what kind of tests you want to write, and these use cases require different tools.

      Reply

Leave a Comment