Introduction to Spock Specifications
After we have finished this lesson, we
- Know how we can create specification classes.
- Are familiar with the structure of a specification class.
- Understand how we should use different fields.
- Can use fixture methods.
This lesson assumes that:
- You are familiar with JUnit 4 categories
- You can run unit tests which use Spock Framework with Maven or Gradle
Watch the Lesson
The text version of this lesson is given in the following:
Creating Our First Spock Specification
The class that contains our test cases is called a specification class, and we can create a Spock specification class by following these steps:
- Create a new Groovy class.
- Extend the
Specification
class. - Configure the category of the test cases found from our specification class.
After we have created a simple specification class, the source code of the created class looks as follows:
import org.junit.experimental.categories.Category import spock.lang.Specification @Category(UnitTest.class) class ExampleSpec extends Specification { }
There are two things that I want to point before we continue this lesson.
- We don’t have to configure the category of our test cases. However, if we separate our unit, integration, and end-to-end tests by using JUnit 4 categories, we have to do this or otherwise our test cases won’t be run.
- When I name my specification classes, I append the string: ‘Spec’ to the name of the system under specification (aka system under test).
Additional Reading:
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
A spock specification can have the following parts:
- Constants are static fields that are useful when we want to name magic numbers.
- Instance fields are a good place to store objects that belong to the specification’s fixture. An object belongs to the specification’s fixture if we use the object when we write our tests. Also, Spock recommends that we initialize our instance fields when we declare them.
- Fixture methods are responsible for 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 sample demonstrates the structure of a Spock specification class:
import org.junit.experimental.categories.Category import spock.lang.Specification @Category(UnitTest.class) class ExampleSpec extends Specification { //Constants //Fields //Fixture methods //Feature methods //Helper methods }
Additional Reading:
We can now identify the basic building blocks of a Spock specification. Let’s move on and take a closer look at constants and instance fields.
Adding Constants and Instance Fields to Our Specification
Before we can add constants and instance fields to our specification class, we have to understand that Spock Framework has two kinds of instance fields:
- The objects stored in “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 in “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.
Enough with theory. Let’s add some constants and instance fields to our specification class. We can do this by following these steps:
- Add a
static
constant calledMESSAGE
to our specification class and initialize it. This constant contains the expected message that should be returned by thegetMessage()
method of theMessageService
class. - Add a normal instance field to our specification class and initialize it by creating a new
MessageService
object. Because we use a normal instance field, every feature method will get its ownMessageService
object. - Add a shared field to our specification class and initialize it by creating a new
Object
. When we create a shared field, we have to mark the field as shared by annotating it with the@Shared
annotation. Also, because this is a shared field, we expect that every feature method will get the same object.
After we have added these fields to our specification class, its source code looks as follows:
import org.junit.experimental.categories.Category import spock.lang.Shared import spock.lang.Specification @Category(UnitTest.class) class FieldExampleSpec extends Specification { static MESSAGE = 'Hello World!' def messageService = new MessageService() @Shared sharedObject = new Object() }
Additional Reading:
Let’s demonstrate the difference between a normal and shared instance field by adding two feature methods to our specification class. These feature methods ensure that the getMessage()
method of the MessageService
class returns the expected message. However, the thing that interests us the most is that both feature methods write objects stored in the messageService
and sharedObject
fields to System.out
.
After we have added these feature methods to our specification class, its source code looks as follows:
import org.junit.experimental.categories.Category import spock.lang.Shared import spock.lang.Specification @Category(UnitTest.class) class FieldExampleSpec extends Specification { static MESSAGE = 'Hello World!' def messageService = new MessageService() @Shared sharedObject = new Object() def 'Get message one'() { println 'First feature method' println 'unique object: ' + messageService println 'shared object: ' + sharedObject expect: 'Should return the correct message' messageService.getMessage() == MESSAGE } def 'Get message two'() { println 'Second feature method' println 'unique object: ' + messageService println 'shared object: ' + sharedObject expect: 'Should return the correct message' messageService.getMessage() == MESSAGE } }
Feature methods are described in the lesson of this topic. However, these feature methods are so simple that you should be able to understand what these feature methods are doing.
When we run our specification class, we should see that the following lines are written to System.out
:
First feature method unique object: com.testwithspring.master.MessageService@6b419da shared object: java.lang.Object@60dcc9fe Second feature method unique object: com.testwithspring.master.MessageService@309e345f shared object: java.lang.Object@60dcc9fe
In other words, we can see that:
- The object that is stored in the normal instance field is not shared between feature methods.
- The object that is stored in the shared instance field is shared between feature methods.
Additional Reading:
Even though we can now add fields to our specification class, we cannot write useful 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
Fixture methods are used to configure and clean up the system under specification and all fixture methods supported by the Spock Framework are optional.
Let’s add all fixture methods to our specification class. A Spock specification can have the following fixture methods:
- The
setupSpec()
method is invoked before the first feature method is invoked. This method is typically used for performing resource intensive configuration that is shared by several feature methods. - The
setup()
method is invoked before every feature method. We can use this method for configuring the system under specification. Also, if our normal instance fields require setup code that is longer than one line, we should initialize them in this method. - The
cleanup()
method is invoked after every feature method. We can use this method for cleaning up the configuration that was done in thesetup()
method. - The
cleanupSpec()
method is invoked after all feature methods have been invoked. This is method used for cleaning up the configuration that was done in thesetupSpec()
method.
After we have added all fixture methods to our specification class, its source code looks as follows:
import org.junit.experimental.categories.Category import spock.lang.Specification @Category(UnitTest.class) class FixtureMethodExampleSpec extends Specification { static MESSAGE = 'Hello World!' def messageService = new MessageService() def setupSpec() { println 'Before the first feature method' } def setup() { println 'Before every feature method' } def cleanup() { println 'After every feature method' } def cleanupSpec() { println 'After the last feature method' } }
The setupSpec()
and cleanupSpec()
methods can use only shared instance fields.
Let’s add two feature methods to our specification class. These feature methods ensure that the getMessage()
method of the MessageService
class returns the expected message. Also, both feature methods write a String
to System.out
.
After we have written these feature methods, the source code of our specification class looks as follows:
import org.junit.experimental.categories.Category import spock.lang.Specification @Category(UnitTest.class) class FixtureMethodExampleSpec extends Specification { static MESSAGE = 'Hello World!' def messageService = new MessageService() def setupSpec() { println 'Before the first feature method' } def setup() { println 'Before every feature method' } def cleanup() { println 'After every feature method' } def cleanupSpec() { println 'After the last feature method' } def 'Get message one'() { println 'First feature method' expect: 'Should return the correct message' messageService.getMessage() == MESSAGE } def 'Get message two'() { println 'Second feature method' expect: 'Should return the correct message' messageService.getMessage() == MESSAGE } }
When we run our specification, we notice that the following lines are written to System.out
:
Before the first feature method Before every feature method First feature method After every feature method Before every feature method Second feature method After every feature method After the last feature method
This output proves that the methods of our specification class are invoked in this order:
setupSpec()
setup()
- First feature method
cleanup()
setup()
- Second feature method
cleanup()
cleanupSpec()
Additional Reading:
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us five things:
- Every Spock specification class must extend the
spock.lang.Specification
class. - A Spock specification can have constants, 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.
Previous LessonNext Lesson