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

When we are writing unit tests with Spock Framework, we have to create so called specifications that describe the features of our application.

This blog post provides an introduction to Spock specifications, and will help us to create our first specification and understand its structure.

Let's start by creating our first Spock specification.

Additional Reading:

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

Creating a Spock Specification

We can create a Spock specification by creating a Groovy class that extends the spock.lang.Specification class. Because we configured our Gradle and Maven projects to run Spock tests found from classes whose names end with the suffix: 'Spec', we have to create the name of our Spock specification class by following this rule: [The name of the tested/specified unit]Spec.

The source code of our specification class looks as follows:

import spock.lang.Specification

class ExampleSpec extends Specification {

}

We have just created our first Spock specification. Unfortunately our specification is useless because it doesn't do anything. Before we can change that, we have to take a closer look at the structure of a Spock specification.

The Structure of a Spock Specification

Every specification can have the following parts:

  • Instance fields are a good place to store objects that belong to the specification's fixture (i.e. we use them when we write our tests). Also, Spock recommends that we initialize our instance fields when we declare them.
  • Fixture methods are responsible of configuring the system under specification (SUS) before feature methods are invoked and cleaning up of the system under specification after feature methods have been invoked.
  • Feature methods specify the expected behavior of the system under specification.
  • Helper methods are methods that are used by the other methods found from the specification class.

The following code listing illustrates the structure of our specification:

import spock.lang.Specification
 
class ExampleSpec extends Specification {
	 //Fields
	 //Fixture methods
	 //Feature methods
	 //Helper methods
}
Additional Reading:

We are now aware of the basic building blocks of a Spock specification. Let's move on and take a closer look at instance fields.

Adding Fields Into Our Specification

We already know that

  • Instance fields are a good place to store objects that belong to the specification's fixture.
  • We should initialize them when we declare them.

However, we have to learn one thing before we can add fields into our specification. A specification can have two types of instance fields:

  • The objects stored into "normal" instance fields are not shared between feature methods. This means that every feature method gets its own object. We should prefer normal instance fields because they help us to isolate feature methods from each other.
  • The objects stored into "shared" instance fields are shared between feature methods. We should use shared fields if creating the object in question is expensive or we want to share something with all feature methods.

Let's add two instance fields into our specification. We can do this by following these steps:

  1. Add a "normal" field (uniqueObject) into the ExampleSpec class and initialize it with a new Object.
  2. Add a shared field (sharedObject) into the ExampleSpec class and initialize it with a new Object. Mark the field as shared by annotating it with the @Shared annotation.

The source code of our specification class looks as follows:

import spock.lang.Shared
import spock.lang.Specification

class ExampleSpec extends Specification {

    def uniqueObject = new Object();
    @Shared sharedObject = new Object();
}

Let's demonstrate the difference of these fields by adding two feature methods into our specification. These feature methods ensure that the toLowerCase() and toUpperCase() methods of the String class are working as expected. However, the thing that interests us the most is that both feature methods write objects stored into the uniqueObject and sharedObject fields to System.out.

Feature methods are described in the next part of this tutorial. However, these feature methods are so simple that you should be able to understand them.

The source code of our specification looks as follows:

import spock.lang.Shared
import spock.lang.Specification

class ExampleSpec extends Specification {

    def message = "Hello world!"

    def uniqueObject = new Object();
    @Shared sharedObject = new Object();

    def "first feature method"() {
        println "First feature method"
        println "unique object: " + uniqueObject
        println "shared object: " + sharedObject

        when: "Message is transformed into lowercase"
        message = message.toLowerCase()

        then: "Should transform message into lowercase"
        message == "hello world!"
    }

    def "second feature method"() {
        println "Second feature method"
        println "unique object: " + uniqueObject
        println "shared object: " + sharedObject

        when: "Message is transformed into uppercase"
        message = message.toUpperCase()

        then: "Should transform message into uppercase"
        message == "HELLO WORLD!"
    }
}

When we run our specification, we should see that the following lines are written to System.out:

First feature method
unique object: java.lang.Object@5bda8e08
shared object: java.lang.Object@3b0090a4
Second feature method
unique object: java.lang.Object@367ffa75
shared object: java.lang.Object@3b0090a4

In other words, we can see that:

  • The object that is stored into the normal instance field is not shared between feature methods.
  • The object that is stored into the shared instance field is shared between feature methods.
Additional Reading:

Even though we can now add fields into our specification, we cannot write useful unit tests because we don't know how we can configure or clean up the system under specification. It's time to find out how we can use fixture methods.

Using Fixture Methods

When we want to configure the system under specification before feature methods are invoked and/or clean up of the system under specification after feature methods have been invoked, we have to use fixture methods.

A Spock specification can have the following fixture methods:

  • The setupSpec() method is invoked before the first feature method is invoked.
  • The setup() method is invoked before every feature method.
  • The cleanup() method is invoked after every feature method.
  • The cleanupSpec() method is invoked after all feature methods have been invoked.

The source code of our specification class, which has all fixture methods, looks as follows:

import spock.lang.Shared
import spock.lang.Specification

class ExampleSpec extends Specification {

    def setup() {
        println "Setup"
    }

    def cleanup() {
        println "Clean up"
    }

    def setupSpec() {
        println "Setup specification"
    }

    def cleanupSpec() {
        println "Clean up specification"
    }
}

When we run our specification, we notice that the following lines are written to System.out:

Setup specification
Clean up specification

In other words, only the setupSpec() and cleanupSpec() methods are invoked. The reason for this is that our specification has no feature methods. That is why the setup() and cleanup() methods are not invoked.

Let's add two feature methods into our specification. These feature methods ensure that the toLowerCase() and toUpperCase() methods of the String class are working as expected. Also, both feature methods writes an "identifier" to System.out.

The source code of our specification looks as follows:

import spock.lang.Shared
import spock.lang.Specification

class ExampleSpec extends Specification {

    def message = "Hello world!"

    def setup() {
        println "Setup"
    }

    def cleanup() {
        println "Clean up"
    }

    def setupSpec() {
        println "Setup specification"
    }

    def cleanupSpec() {
        println "Clean up specification"
    }

    def "first feature method"() {
        println "First feature method"

        when: "Message is transformed into lowercase"
        message = message.toLowerCase()

        then: "Should transform message into lowercase"
        message == "hello world!"
    }

    def "second feature method"() {
        println "Second feature method"

        when: "Message is transformed into uppercase"
        message = message.toUpperCase()

        then: "Should transform message into uppercase"
        message == "HELLO WORLD!"
    }
}

When we run our specification, we notice that the following lines are written to System.out:

Setup specification
Setup
First feature method
Clean up
Setup
Second feature method
Clean up
Clean up specification

This proves that the fixture methods are invoked in the order that was described in the beginning of this section.

Additional Reading:

Let's move on and summarize what we learned from this blog.

Summary

This blog post has taught us five things:

  • Every Spock specification must extend the spock.lang.Specification class.
  • A Spock specification can have instance fields, fixture methods, feature methods, and helper methods.
  • We should prefer normal instance fields because they help us to isolate feature methods from each other.
  • We should use shared instance fields only if creating the object in question is expensive or we want to share something with all feature methods.
  • We can initialize and clean up the system under specification by using fixture methods.

The next part of this tutorial provides an introduction to feature methods that are the heart of a specification class.

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

11 comments… add one
  • Umesh Joshi Mar 23, 2017 @ 16:34

    Nice to learn basic stuff quickly... well written.. helped me a lot.. thanks

    • Petri Mar 27, 2017 @ 21:02

      You are welcome! I am happy to hear that this blog post was useful to you.

  • learner Feb 13, 2018 @ 7:23

    When we run our specification, we notice that the following lines are written to System.out:

    Could you please specify how to run the specification,

    Are they run like unit tests?
    When I ran the "mvn clean test" as specified in the README, nothing happened.

    • Petri Feb 13, 2018 @ 8:28

      Hi,

      When you say that nothing happens when you run the command: mvn clean test, what do you mean? Do you see an error message? If so, what is it?

      The reason why I ask this is that I am able to run the tests of this example by using the command: mvn clean test.

      By the way, if you want to get more information about the Maven build, you should take a look at this blog post.

  • learner Feb 13, 2018 @ 21:43

    Hi Petri
    I cloned your example from GitHub, and ran the unit tests from Maven-->Edit configuration --> 'Clean test' as Command line parameter. I though can do the same for this example: http://www.petrikainulainen.net/programming/testing/writing-unit-tests-with-spock-framework-introduction-to-specifications-part-one/, but it did not work.

    My project has not been detected as a Maven project, so I tried reimporting it by opening the pom.xml using File > Open. IntelliJ reopened the project using the Maven structure. Then I could run the tests.

    Thank you very much

    • learner Feb 13, 2018 @ 22:20

      Thank you for the explanation and your blog is a rock star. I like it and the working example is awesome! Keep blogging. :)))

  • Viswanathan Mar 31, 2021 @ 8:59

    Hi Petri,
    Please let me know the link for the next tutorial related to feature methods

Leave a Reply