Spock Tutorial for Beginners
Table Of Contents
1. Introduction
Spock is a testing framework for Java and Groovy applications. It’s very own Junit Runner, Sputnik, makes it compatible with most IDEs, build tools, and continuous integration servers.
2. Environment
This tutorial assumes that you have basic understanding of Gradle build framework and also that your Eclipse IDE (Luna) environment is fully setup and configured with
3. Learn By Example
To make this learning process a little interesting, let us do so using a basic example.
For this purpose I would get you started with creation of a Gradle project and configure the build.gradle
before you start writing your Spock specifications.
3.1. Create and configure a Gradle project in Eclipse IDE
In the Eclipse IDE, click File -> New -> Other
Select “Gradle Project”
Take a moment to read the suggestions in the following screen. Press next.
Enter the name of your project.
Keep the default and recommended Gradle Wrapper option selected and press next.
Press finish on the preview screen.
You have successfully created the Gradle project. The following is the project structure of your Gradle project.
The unit tests written using Junit are placed under folder src/test/java
folder. The Groovy unit tests written using Spock framework are placed under src/test/groovy
folder. Note that the default Gradle project structure do not have src/test/groovy
folder. This will be created at later stage.
To enable your Gradle project to compile and run both Junit tests and Groovy tests include the following required dependencies in build.gradle
.
dependencies { testCompile ("junit:junit:${junitVersion}") testCompile("org.codehaus.groovy:groovy-all:${groovyVersion}") testCompile("org.spockframework:spock-core:${spockFrameworkVersion}") }
Create gradle.properties
to configure dependencies versions as shown in the following few steps.
Right-mouse click on project SpoockyOne. Select New -> File as shown below.
Select the project root folder and enter the filename gradle.properties
.
Configure the required dependencies versions in gradle.properties
, as shown below.
gradle.properties
# dependencies versions spockFrameworkVersion=1.0-groovy-2.4 groovyVersion=2.4.5 junitVersion=4.12 slf4jVersion=1.7.13 logbackCoreVersion=1.1.3 logbackClassicVersion=1.1.3
After including all the required dependencies, the complete build.gradle
looks like this.
build.gradle
apply plugin: 'groovy' version = "1.0" description = "SpoockyOne" sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenLocal() jcenter() } sourceSets { main { java.srcDir "src/main/java" } test { java.srcDir "src/test/groovy" } } dependencies { compile("org.slf4j:slf4j-api:${slf4jVersion}") testRuntime("ch.qos.logback:logback-core:${logbackCoreVersion}") testRuntime("ch.qos.logback:logback-classic:${logbackClassicVersion}") testCompile ("junit:junit:${junitVersion}") testCompile("org.codehaus.groovy:groovy-all:${groovyVersion}") testCompile("org.spockframework:spock-core:${spockFrameworkVersion}") }
Let us walk through the build.gradle
.
apply plugin: 'groovy'
The “groovy” plugin extends “java” plugin. This plugin supports joint compilation to allow the flexibility of combining Groovy and Java code with bi-directional dependency. A groovy class can extend a java class and vice versa.
version = "1.0" description = "SpoockyOne"
These are the project standard properties and are used in the build script to provide version and description of the project.
sourceCompatibility = 1.8 targetCompatibility = 1.8
This build script specifies the source and target compatibility versions used for compiling Java sources.
sourceSets { main { java.srcDir "src/main/java" } test { java.srcDir "src/test/groovy" } }
As you can see from the gradle project structure, src/test/groovy
doesn’t exist by default. You have to create this folder src/test/groovy
and add it to the sourceSets
build script block in build.gradle
. The above build script block specifies that the java source is under the folder src/main/java
and the test scripts source is to be found under src/test/groovy
.
jcenter()
When jcenter()
is included in build.gradle
, Gradle looks for the configured libraries and their dependencies in JCenter Maven repository (https://jcenter.bintray.com).
mavenLocal()
mavenLocal()
can be specified to use local Maven cache as a repository.
compile("org.slf4j:slf4j-api:${slf4jVersion}") testRuntime("ch.qos.logback:logback-core:${logbackCoreVersion}") testRuntime("ch.qos.logback:logback-classic:${logbackClassicVersion}")
To configure slf4j with logback as logging framework.
testCompile("org.codehaus.groovy:groovy-all:${groovyVersion}")
In this case Groovy is only used for test code and thus the Groovy dependency should be added to the testCompile
configuration.
compile("org.codehaus.groovy:groovy-all:${groovyVersion}")
If Groovy is used for production code, the Groovy dependency should be added to the compile configuration.
testCompile("org.spockframework:spock-core:${spockFrameworkVersion}")
This is to specify the Spock dependency version that needs to be downloaded from the repository.
Your gradle project is ready for build.
3.2. Build your project
Issue the following command at command prompt to build the project.
gradlew clean build
The output of this command is shown below. This build process pulls the required libraries that are configured in build.gradle
and resolves their dependencies.
As you can see in the following screen shot, the dependencies of the required libraries mentioned in build.gradle
are also downloaded.
For further reading about how these dependencies are resolved, follow the link Gradle dependency management.
3.3. Let’s get Groovy and Spocky
In the next few sections, we will create the following required java classes and interfaces.
Book.java
BookService.java
BookRepository.java
BookServiceImpl.java
BookServiceImpl
, the implementation of BookService
interface is the “System Under Specification” – the class which we are going to write specification for.
3.3.1. Create Book.java
Create a new java class Book.java
as shown in the following screen shots.
Right-mouse click on the folder src/main/java
. Select File -> New -> Other as shown below.
Select “Class”
Specify the package name and java class name and click “Finish”.
Book.java
package com.readerscorner.model; public class Book { Integer bookId; String title; String author; public Book(Integer bookId, String title, String author) { super(); this.bookId = bookId; this.title = title; this.author = author; } public Integer getBookId() { return bookId; } public void setBookId(Integer bookId) { this.bookId = bookId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } @Override public String toString() { return "Book [bookId=" + bookId + ", title=" + title + ", author=" + author + "]"; } }
3.3.2. Create BookService.java
Create a java interface with name BookService.java
as shown in the following screen shots.
Right-mouse click on the folder src/main/java
. Select File -> New -> Other as shown below.
Select “Interface”
Specify the package name and the interface name as shown below.
BookService.java
package com.readerscorner.service; import com.readerscorner.model.Book; public interface BookService { Book retrieveBookDetails(Integer bookId); }
3.3.3. Create BookRepository.java
Right-mouse click on the folder src/main/java
. Select File -> New -> Other as shown below.
Select “Interface”
Specify the package name and the interface name as shown below.
BookRepository.java
package com.readerscorner.repository; import com.readerscorner.model.Book; public interface BookRepository { Book getBook(Integer bookId); }
The project structure after creating the required classes is shown below.
Please note that we haven’t yet written the BookServiceImpl.java
which is the implementation for BookService
.
3.3.4. Create BookServiceSpecification
Let us write the specification with name BookServiceSpecification
to describe the expected features of our “System Under Specification”(SUS), BookServiceImpl
, i.e, the implementation of BookService
interface.
Spock specifications are written as groovy classes and placed under the folder src/test/groovy
.
Create a groovy class BookServiceSpecification.groovy
as shown in the following screens.
Create a folder src/test/groovy
.
Enter the folder name “groovy” and press “Finish”.
Build the project once again using
gradlew clean build
Try to refresh the project in Eclipse IDE using “Refresh Gradle Project” as shown below.
Right-mouse click on src/test/groovy
folder. Select New->Other.
Select “Groovy Class”.
Specify the package name and Specification name as shown below.
Please note the warning displayed. “SpoockyOne is not a groovy project. Groovy Nature will be added to project upon completion.”
Please have a look at the project structure after creating the first Groovy class.
3.3.5. Red -> Green -> Refactor
Let us follow the TDD cycle to progressively write and test our System Under Specification.
Step 1 – RED – Write a test that will fail unless that behaviour is present. In Step1, we will write the BookServiceSpecification.groovy
without any BookServiceImpl
implementation. When BookServiceSpecification
is run, it will fail because of this reason.
Step 2/Step 3 – Green – Write just enough code to get the test to pass. In Step 2 and step 3, we will add basic implementation of BookServiceImpl
and make the test BookServiceSpecification
pass.
Step 4 – Refactor – As you have minimum code that works, refactor it further to achieve desired design.
3.3.5.1 Step 1
3.3.5.1.1 Write Specification
BookServiceSpecification.groovy
package com.readerscorner.test import com.readerscorner.model.Book import com.readerscorner.service.BookService; import com.readerscorner.repository.BookRepository; import spock.lang.Specification import groovy.util.logging.Slf4j @Slf4j class BookServiceSpecification extends Specification { BookService bookService BookRepository bookRepository def setupSpec(){ log.debug("setupSpec() - Runs once per Specification"); } def setup(){ log.debug ("setup() - Runs before every feature method"); bookService = null; bookRepository = Stub(BookRepository); bookRepository.getBook(_) >> { int id -> if (id == 1) { Book b = new Book(1, 'Srujana', 'Spock Tut'); log.debug(b.toString()); return b; } else if (id == 2) { Book b = new Book(2, 'Eugen', 'JUnit Tut'); log.debug(b.toString()); return b; } else if (id == 3) { log.debug("Book with this ID does not exist"); return null; } } } def "retrieved book object is not null"(){ log.debug ("Feature method 1 - retrieved book object is not null- start"); expect : bookService.retrieveBookDetails(id) != null where : id << [1, 2] } def "retrieved book object is null"(){ log.debug ("Feature method - 2 retrieved book object is null - start"); expect : bookService.retrieveBookDetails(id) == null where : id << 3 } def cleanup(){ log.debug ("Cleanup method - Runs after every feature method."); } def cleanupSpec(){ log.debug ("cleanupSpec() - Runs only once per specification"); } }
3.3.5.1.2 Run the test
There are two ways you can run this test.
- Use Eclise IDE: Select the
BookServiceSpecification.groovy
class. Right mouse click and Select Run as -> Junit Test.
This test would fail with
NullPointerException
for obvious reasons. ThebookService
object is null and the specification is invokingretrieveBookDetails()
method on this null object.
- At the command prompt use one of the following commands to run the test.
gradlew clean test
gradlew --stacktrace clean test
As mentioned in above point 1, this test would FAIL. Following is the output of the test.
As mentioned in the above command window screen shot,
$PROJECT_HOME\build\reports\tests\index.html
should display the test summary. Following is the screenshots of the test summary of the above failed test.
3.3.5.2 Step 2
3.3.5.2.1 Create/Refactor BookServiceImpl and BookServiceSpecification.
In this step we will make our specification pass by creating/refactoring BookServiceImpl
class. Create new java class BookServiceImpl.java
in Eclipse IDE as shown below.
Right-mouse click on the folder src/main/java
. Select File -> New -> Other as shown below.
Select “Class”
Specify the details as shown below click “Finish”.
BookServiceImpl.java
BookServiceImpl.java package com.readerscorner.impl; import com.readerscorner.model.Book; import com.readerscorner.repository.BookRepository; import com.readerscorner.service.BookService; public class BookServiceImpl implements BookService { BookRepository bookRepository; void setBookRepository(BookRepository bRepository){ bookRepository = bRepository; } @Override public Book retrieveBookDetails(Integer bookId) { return bookRepository.getBook(bookId); } }
And modify BookServiceSpecification.groovy
as follows.
BookServiceSpecification.groovy
package com.readerscorner.test import com.readerscorner.impl.BookServiceImpl; import com.readerscorner.model.Book import com.readerscorner.service.BookService; import com.readerscorner.repository.BookRepository; import spock.lang.Specification import groovy.util.logging.Slf4j @Slf4j class BookServiceSpecification extends Specification { BookServiceImpl bookService BookRepository bookRepository def setupSpec(){ log.debug("setupSpec() - Runs once per Specification"); } def setup(){ log.debug ("setup() - Runs before every feature method"); bookService = new BookServiceImpl(); bookRepository = Stub(BookRepository); bookService.setBookRepository(bookRepository) bookRepository.getBook(_) >> { int id -> if (id == 1) { Book b = new Book(1, 'Srujana', 'Spock Tut'); log.debug(b.toString()); return b; } else if (id == 2) { Book b = new Book(2, 'Eugen', 'JUnit Tut'); log.debug(b.toString()); return b; } else if (id == 3) { log.debug("Book with this ID does not exist"); return null; } } } def "retrieved book object is not null"(){ log.debug ("Feature method 1 - retrieved book object is not null- start"); expect : bookService.retrieveBookDetails(id) != null where : id << [1, 2] } def "retrieved book object is null"(){ log.debug ("Feature method - 2 retrieved book object is null - start"); expect : bookService.retrieveBookDetails(id) == null where : id << 3 } def cleanup(){ log.debug ("Cleanup method - Runs after every feature method."); } def cleanupSpec(){ log.debug ("cleanupSpec() - Runs only once per specification"); } }
3.3.5.2.2 Run the test
- Build the project from command prompt using the following command:
gradlew clean build
- Refresh the project – Right-mouse click on project root SpoockyOne – Select “Gradle” -> “Refresh Gradle Project”
- In Eclipse IDE – Run as -> “JUnit Test”.
- From command prompt issue the following command:
gradlew clean test
The test summary report for the passed tests is shown below.
3.3.5.3 Step 3
3.3.5.3.1 Modify BookServiceSpecification
Modify BookServiceSpecification.groovy
to include test for exception handling.
BookServiceSpecification.groovy
package com.readerscorner.test import com.readerscorner.impl.BookServiceImpl; import com.readerscorner.model.Book import com.readerscorner.service.BookService; import com.readerscorner.repository.BookRepository; import spock.lang.Specification import groovy.util.logging.Slf4j @Slf4j class BookServiceSpecification extends Specification { BookServiceImpl bookService BookRepository bookRepository def setupSpec(){ log.debug("setupSpec() - Runs once per Specification"); } def setup(){ log.debug ("setup() - Runs before every feature method"); bookService = new BookServiceImpl(); bookRepository = Stub(BookRepository); bookService.setBookRepository(bookRepository) bookRepository.getBook(_) >> { int id -> if (id == 1) { Book b = new Book(1, 'Srujana', 'Spock Tut'); log.debug(b.toString()); return b; } else if (id == 2) { Book b = new Book(2, 'Eugen', 'JUnit Tut'); log.debug(b.toString()); return b; } else if (id == 3) { log.debug("Book with this ID does not exist"); return null; } else if (id <= 0) { throw new IllegalArgumentException("Invalid Book ID"); } } } def "retrieved book object is not null"(){ log.debug ("Feature method 1 - retrieved book object is not null- start"); expect : bookService.retrieveBookDetails(id) != null where : id << [1, 2] } def "retrieved book object is null"(){ log.debug ("Feature method - 2 retrieved book object is null - start"); expect : bookService.retrieveBookDetails(id) == null where : id << 3 } def "book id must be greater than 0"(){ log.debug ("Feature method 3 - book id must be greater than 0 - start"); given : //NA when: bookService.retrieveBookDetails(-3) then: thrown(IllegalArgumentException) } def cleanup(){ log.debug ("Cleanup method - Runs after every feature method."); } def cleanupSpec(){ log.debug ("cleanupSpec() - Runs only once per specification"); } }
3.3.5.3.2 Run the test
- Build the project from command prompt using the following command:
gradlew clean build
- Refresh the project – Right-mouse click on project root SpoockyOne – Select “Gradle” -> “Refresh Gradle Project”
- In Eclipse IDE – Run as -> “JUnit Test”.
- From command prompt issue the following command:
gradlew clean test
The test summary report for the passed tests is shown below.
3.3.5.4. Step 4
To keep it simple and straight further refactoring of the project is omitted from here. In our example, in Step 4:
- we could further refactor the project by including implementation for
BookRepository
interface - Modify the specification to further test the interaction between
BookServiceImpl
andBookRepository
implementation.
This Step 4 could be a take-home for you as your homework.
3.3.5.5. Get to Know BookServiceSpecification
Let’s step through the BookServiceSpecification.groovy
import groovy.util.logging.Slf4j @Slf4j
This annotation is to use logging in the specification. Build.gradle
is needed to be configured to use slf4j and the logging framework of choice (logback in this example). Though in this example the logging is done to standard output(which is by default), you may configure it to log to a log file of choice.
import spock.lang.*
The spock.lang
package has the required types needed for writing specifications
class BookServiceSpecification extends Specification { … }
As shown in our example, a specification is written as a Groovy class. The spock.lang.Specification
is the base class for all Spock specifications. The spock.lang.Specification
class not only has methods needed to write specification but also instructs JUnit to run tests using Spock’s native Junit runner, Sputink.
def setupSpec(){} def setup(){} def cleanup(){} def cleanupSpec(){}
These methods are called “fixture methods”. These are used to setup and clean up the environment in which the feature methods are run. As shown in the example:
setupSpec()
– runs once before the first feature methodcleanupSpec()
– runs once after the first feature methodsetup()
– runs before every feature methodcleanup()
– runs after every feature method.
def "retrieved book object is not null"(){} def "retrieved book object is null"(){} def "book id must be greater than 0"(){}
These are called “feature methods”. These describe the expected behavior of the system under specification. As shown in the example, choose the names wisely based on the behavior you are specifying.
Every feature method always has a “stimulus” block and a “response” block defined. For example, when
block is used to provide “stimulus” to the feature method and then
is used to provide the expected “response”.
when: bookService.retrieveBookDetails(-3) then: thrown(IllegalArgumentException)
Yet another example is, expect
block where the “stimulus” and “response” is mentioned in a single expression.
expect : bookService.retrieveBookDetails(id) == null
As shown in our example the feature methods, retrieved book object is not null
and retrieved book object is null
can be referred as “data driven feature methods”. They both used where
block which defines a set of data. The where
block defines multiple versions of the feature method.
For example,
where : id << [1, 2]
This where
block creates two versions of the feature method, one where id=1 and another where id=2. Where
block always comes last in the feature method and may not be repeated.
bookRepository = Stub(BookRepository); bookRepository.getBook(_) >> { }
Here we are stubbing the getBook()
method of BookRepository
to make it respond to method calls in certain way. As you would have observed, BookRepository
interface is never implemented so far. We are creating Stub of BookRepository
and stubbing the behaviour of getBook()
method.
We stubbed the method in such a way that it behaves different for different values of parameter id.
For detailed reading of other concepts such as fixture methods, feature methods, blocks and many other interesting features of Spock, please follow the docs at Spock Framework reference documentation
4. References
If you would like to view, edit, run, and even publish your Spock specifications without setting up your own workspace environment, use the link:
I would recommend you to follow the below links for additional reading.
- Spock Framework reference documentation
- Spock Framework
- Spock example
- Groovy Documentation
- Gradle Documentation
- Gradle dependency management
- Groovy Plugin
- User discussion group
- Dev discussion group
5. Conclusion
We had created a basic Spock specification with few fixture and feature methods. You may continue developing your skills with further reading at the links provided in the above section. Please do not forget to do your homework. Enjoy!!
6. Download the Eclipse project
You can download the full source code of this example here: Spock – Beginners Tutorial
Thank you so much, Srujana for putting together this great tutorial. Until reading this, I had only modified other people’s Gradle/Groovy/Spock projects. I was struggling with creating a new one from scratch, and also struggling with finding an online resource that started at the beginning. This was exactly what I needed. I wish I had found it sooner!