When creating an application with Spring Boot, our to-go unit testing framework is JUnit and Mockito for mocking dependencies.
For me personally, I like using the Spock Framework for my unit tests. With Spock you can take advantage of the Groovy language syntax and enhanced by Spock through Groovy's metaprogramming features. This combination of features makes your unit tests more expressive and easier to read (and write!)
Setting up Spock in Gradle
In the plugins section of the build.gradle
file, the groovy
plugin must be installed. This tells Gradle to look at src/{main,test}/groovy
directories for Groovy source codes to compile.
plugins {
id 'groovy'
// other plugins...
}
Next, we also need to install Spock-related dependencies:
dependencies {
// other dependencies...
testImplementation libs.springBoot.test
testImplementation libs.bundles.spock
}
Notice in my example that I am using Gradle's dependency version catalog. Here is how libs.springBoot.test
and libs.bundles.spock
is declared in the settings.gradle
file:
Using the dependency version catalog is a choice. Therefore, installing dependencies using the group:artifact:version
format is valid:
dependencies {
// other dependencies...
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation "org.spockframework:spock-core:${spockVersion}"
testImplementation "org.spockframework:spock-spring:${spockVersion}"
}
Now that we installed the dependencies to use Spock (spock-core
), use Spring-related features (spock-spring
), and its runner platform (spring-boot-starter-test
), we can now create unit tests with Spock!
Write Unit Test Files
Create a Groovy test file (or what Spock calls a Specification) in the src/test/groovy
directory in your project.
Remember that Spock looks for specifications in the groovy
directory, not java
. This works because you are using the 'groovy'
plugin in the build.gradle
file.
Now that the specification file has been created, its time to write our Spock tests using the Groovy language.
Creating the Specification Class
The Specification class defines the methods that will be used to test the system under test.
Just like in JUnit, the test package is the same package as the system under test.
Make sure that the specification class inherits the spock.lang.Specification
class. All Spock specification classes should inherit this class.
The Fixture Methods
A fixture method is used to setup the feature methods before they run. JUnit offers the same capability by using annotations. In Spock, if a method is named with one of the fixture method names, it will work as a fixture.
Spock Fixture Method | JUnit Annotation |
---|---|
setupSpec() |
@BeforeAll |
setup() |
@BeforeEach |
cleanupSpec() |
@AfterAll |
cleanup() |
@AfterEach |
Unlike in Mockito that you can just use @InjectMocks
to setup mocks in the system under test, you have to setup mocks manually in Spock. This is done in the setup()
fixture:
Creating the Feature Methods
Feature method tests the system under test. Unlike JUnit where you need to annotate the test methods with @Test
, Spock treats public methods with blocks as a feature method.
By convention, feature methods are named using String literals (a feature of the Groovy language). This will help describe the feature in a more expressive manner.
Blocks
In the example above there are lines with given:
, when:
, and then:
labels. Those labels signifies the start of a block. Blocks are used to divide and implement parts of a test and some Spock magic happens behind the scene on some blocks.
I usually use the given-when-then
blocks when writing tests.
The given
block contains the setup for the feature – declarations and mocks are done here.
The when
block is the stimulus of the system under test. In this block the method under test is called, and sometimes the return value is captured in a variable.
Finally the then
block contains the assert conditions for the feature. Just write boolean expressions in this block, and Spock will interpret them as assertions.
Exception checking is also done in then
block:
def "saveUser should throw exception when given null user"() {
given:
def user = null
when:
userService.saveUser(user)
then:
thrown InvalidUserException
Interaction checks are also done in then
block:
def "saveUser should save valid user to database"() {
given:
def user = buildValidUser()
when:
userService.saveUser(user)
then:
1 * userRespository.save(user)
}
There's also the expect
block that works just like the then
block. It is used to simple one-liner tests:
def "addition should work as usual"() {
expect:
2 == 1 + 1
}
It is also possible to perform a local cleanup of resources using the cleanup
block. I rarely use this block, but when I do it is usually for deleting temp files.
And of course, there is the where
block – the reason why I keep coming back to Spock. The where
block contains setup for the variables that will make the feature parameterized. Unlike in JUnit where you need different @*Source
annotations to setup your parameterized test, you can make it prettier using Spock's where
block:
def "addition should work as usual"() {
expect:
result == a + b
where:
a | b | result
1 | 1 | 2
2 | 3 | 5
}
Behind the scenes, when using a where
block, multiple versions of the feature method is being created with the values of the local variables being different.
Conclusion
Spock is a good alternative to JUnit, and can be easily installed in your Spring Boot project using Gradle (and Maven, which is not shown.)
Make sure to write your specification classes in src/test/groovy
.
With Spock, you can take advantage of Groovy language's powerful features and short-hand syntax. Combine this with Spock powerful features, like parameterizing test with where
block.
Other than what is written in this article, Spock has more feature that sets it apart from other unit testing frameworks. For example is the interaction-based testing and argument capturing that is more consise than Mockito verifications and @Unroll
-ing parameterized methods.
Writing with Spock and Groovy makes your unit tests more readable and maintainable. Remember that Test code is just as important as production code (Robert C. Martin, Clean Code, Chapter 9).